Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
94 changes: 11 additions & 83 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -25,95 +25,23 @@
<SentryWindowsArtifactsDestination>$(SentryArtifactsDestination)Windows/Sentry/</SentryWindowsArtifactsDestination>
</PropertyGroup>

<PropertyGroup>
<HubSecondaryInstallPathFile>$(AppData)\UnityHub\secondaryInstallPath.json</HubSecondaryInstallPathFile>
<HubDefaultEditorFile>$(AppData)\UnityHub\defaultEditor.json</HubDefaultEditorFile>
<HubDefaultEditor Condition="'$(HubDefaultEditor)' == ''"></HubDefaultEditor>
<HubInstallDir Condition="'$(HubInstallDir)' == '' AND $([MSBuild]::IsOSPlatform('Windows'))">C:\Program Files\Unity\Hub\Editor</HubInstallDir>
<HubInstallDir Condition="'$(HubInstallDir)' == '' AND $([MSBuild]::IsOSPlatform('OSX'))">\Applications\Unity\Hub\Editor</HubInstallDir>
<HubInstallDir Condition="'$(HubInstallDir)' == '' AND $([MSBuild]::IsOSPlatform('Linux'))">$(Home)\Unity\Hub\Editor</HubInstallDir>
<HubInstallDir Condition="!Exists('$(HubInstallDir)')"></HubInstallDir>
</PropertyGroup>

<Target Name="FindHub"
Condition="'$(HubInstallDir)' == '' AND Exists('$(HubSecondaryInstallPathFile)')"
Returns="$(HubInstallDir);$(HubDefaultEditor)">

<ReadLinesFromFile File="$(HubSecondaryInstallPathFile)">
<Output TaskParameter="Lines" ItemName="item1" />
</ReadLinesFromFile>

<ReadLinesFromFile File="$(HubDefaultEditorFile)" Condition="Exists('$(HubDefaultEditorFile)')">
<Output TaskParameter="Lines" ItemName="item2" />
</ReadLinesFromFile>

<PropertyGroup>
<HubInstallDir>@(item1->Replace('"', ''))</HubInstallDir>
<HubInstallDir Condition=" !Exists('$(HubInstallDir)') "></HubInstallDir>

<HubDefaultEditor>@(item2->Replace('"', ''))</HubDefaultEditor>
<HubDefaultEditor Condition=" !Exists('$(HubInstallDir)\$(HubDefaultEditor)') "></HubDefaultEditor>
</PropertyGroup>
</Target>

<!-- Use the Unity Editor version set in the sample project of the repo -->
<Target Name="FindUnity" DependsOnTargets="FindHub" AfterTargets="FindHub">
<Target Name="FindUnity">
<Message Text="Unity Version: $(UnityVersion)" Importance="Normal" />

<!-- Find all the installations of Unity done by the Unity Hub -->
<ItemGroup Condition="'$(HubInstallDir)' != '' AND '$(HubDefaultEditor)' == ''">
<_AllUnityInstallDirs Include="$([System.IO.Directory]::GetDirectories('$(HubInstallDir)'))" />
<_UnityInstalls Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(Filename)', '^[\d]{4}.*$'))" Include="@(_AllUnityInstallDirs->'%(Filename)%(Extension)')" />
</ItemGroup>

<!-- Pick the latest one if this version of the Hub doesn't record a default version -->
<PropertyGroup Condition="'$(HubInstallDir)' != '' AND '$(HubDefaultEditor)' == ''">
<HubDefaultEditor>%(_UnityInstalls.Identity)</HubDefaultEditor>
</PropertyGroup>

<PropertyGroup>
<_UnityInnerPath Condition="!$([MSBuild]::IsOSPlatform('OSX'))">Editor\Data</_UnityInnerPath>
<_UnityInnerPath Condition="$([MSBuild]::IsOSPlatform('OSX'))">Unity.App\Contents</_UnityInnerPath>
</PropertyGroup>

<ItemGroup>
<_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())" />
</ItemGroup>
<Exec
Command="pwsh -NoProfile -File &quot;$(RepoRoot)scripts/find-unity.ps1&quot; -UnityVersion &quot;$(UnityVersion)&quot; -HubInstallDir &quot;$(HubInstallDir)&quot; -HubDefaultEditor &quot;$(HubDefaultEditor)&quot;"
ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="_FindUnityOutput" />
</Exec>

<PropertyGroup>
<!--This is a little hack to grab the first item found on the list - properties are repeatedly set for each item on the list, so they end up with the last one (we reversed the list so we get the first one) -->
<_UnityPathProp>%(_UnityPathsFoundReversed.Identity)</_UnityPathProp>
<UnityRoot>$([System.Text.RegularExpressions.Regex]::Match($(_FindUnityOutput), 'UnityRoot=([^;\r\n]+)').Groups[1].Value)</UnityRoot>
<UnityDataPath>$([System.Text.RegularExpressions.Regex]::Match($(_FindUnityOutput), 'UnityDataPath=([^;\r\n]+)').Groups[1].Value)</UnityDataPath>
<UnityManagedPath>$([System.Text.RegularExpressions.Regex]::Match($(_FindUnityOutput), 'UnityManagedPath=([^;\r\n]+)').Groups[1].Value)</UnityManagedPath>
<UnityLibcache>$(UnityDataPath)Resources/PackageManager/ProjectTemplates/libcache/</UnityLibcache>
</PropertyGroup>

<Message
Condition="'$(UnityVersion)' != '' AND '$(HubInstallDir)' != '' AND !Exists('$(HubInstallDir)\$(UnityVersion)\$(_UnityInnerPath)\Managed\UnityEngine.dll') AND '$(_UnityPathProp)' != ''"
Text="Unity version $(UnityVersion) is not installed. Falling back to default Unity installation."
Importance="High" />

<ItemGroup>
<!-- Turn the property back into an item so we can use DirectoryName() below. -->
<_UnityPath Include="$(_UnityPathProp)" />
</ItemGroup>

<PropertyGroup Condition="'$(_UnityPathProp)' != ''">
<UnityManagedPath>@(_UnityPath->DirectoryName())\</UnityManagedPath>
<UnityDataPath>@(_UnityPath->DirectoryName()->DirectoryName())\</UnityDataPath>
<UnityRoot>@(_UnityPath->DirectoryName()->DirectoryName()->DirectoryName())\</UnityRoot>
<UnityLibcache>$(UnityDataPath)Resources\PackageManager\ProjectTemplates\libcache\</UnityLibcache>
</PropertyGroup>

<Error Condition="'$(UnityRoot)' == ''" Text="UnityRoot not found. Ensure Unity is installed.
See the CONTRIBUTING.md.
UnityVersion: '$(UnityVersion)'
Expected to exist:
* @(_PotentialUnityPaths, '%0a or %0a * ')" />

<Error Condition="'$(UnityRoot)' == ''" Text="UnityRoot not resolved — see scripts/find-unity.ps1 output above." />

<!-- Unity paths on Windows -->
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
Expand Down
119 changes: 119 additions & 0 deletions scripts/find-unity.ps1
Original file line number Diff line number Diff line change
@@ -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=<dir containing the Unity executable, trailing slash>
# UnityDataPath=<dir containing the Editor data, trailing slash>
# UnityManagedPath=<dir containing UnityEngine.dll, trailing slash>
#
# 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 <hub>/<version>/ 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
7 changes: 5 additions & 2 deletions src/Sentry.Unity.Android/SentryJava.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AndroidJavaObject>("getLogs"))
{
logsOptions.Call("setEnabled", options.EnableLogs);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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());

Expand Down
30 changes: 30 additions & 0 deletions src/Sentry.Unity/SentryUnityOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,36 @@ public sealed class SentryUnityOptions : SentryOptions
/// </remarks>
public NativeInitializationType AndroidNativeInitializationType { get; set; } = NativeInitializationType.Runtime;

/// <summary>
/// Enables ANR detection on Android through the native (sentry-java) SDK.
/// On API ≥ 30 this uses Android's <c>ApplicationExitInfo</c> to report ANRs detected by the OS
/// in prior runs (ANR v2). On API &lt; 30, sentry-java falls back to its in-process watchdog.
/// The Unity SDK's C# ANR watchdog continues to run on all API levels.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public bool AndroidNativeAnrEnabled { get; set; } = true;

/// <summary>
/// When <see cref="AndroidNativeAnrEnabled"/> is enabled, controls whether sentry-java reports historical ANRs
/// recorded by the OS (<c>ApplicationExitInfo</c>) from prior runs. Has no effect when
/// <see cref="AndroidNativeAnrEnabled"/> is <c>false</c>.
/// </summary>
/// <remarks>
/// Runtime-only. There is no <c>AndroidManifest</c> meta-data tag for this option, so it is only
/// applied when <see cref="AndroidNativeInitializationType"/> is <see cref="NativeInitializationType.Runtime"/>.
/// </remarks>
public bool AndroidReportHistoricalAnrs { get; set; } = false;

/// <summary>
/// When <see cref="AndroidNativeAnrEnabled"/> is enabled, controls whether sentry-java attaches a thread dump
/// to ANR events. Has no effect when <see cref="AndroidNativeAnrEnabled"/> is <c>false</c>.
/// </summary>
public bool AndroidAttachAnrThreadDump { get; set; } = false;

/// <summary>
/// Whether the SDK should add the NDK integration for Android
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,41 @@ public void ModifyManifest_UnityOptions_AndroidNativeSupportEnabled_InitTypeBuil
StringAssert.Contains($"<meta-data android:name=\"io.sentry.dsn\" android:value=\"{_fixture.SentryUnityOptions.Dsn}\" />", 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("<meta-data android:name=\"io.sentry.anr.enable\" android:value=\"True\" />", manifest);
StringAssert.Contains("<meta-data android:name=\"io.sentry.enable-scope-persistence\" android:value=\"True\" />", 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("<meta-data android:name=\"io.sentry.anr.enable\" android:value=\"False\" />", manifest);
StringAssert.Contains("<meta-data android:name=\"io.sentry.enable-scope-persistence\" android:value=\"False\" />", manifest);
}

[Test]
public void ModifyManifest_AndroidAttachAnrThreadDump_FlowsThroughToManifest()
{
_fixture.SentryUnityOptions!.AndroidAttachAnrThreadDump = true;
var sut = _fixture.GetSut();

var manifest = WithAndroidManifest(basePath => sut.ModifyManifest(basePath));

StringAssert.Contains("<meta-data android:name=\"io.sentry.anr.attach-thread-dumps\" android:value=\"True\" />", manifest);
}

[Test]
public void ModifyManifest_ManifestHasDsn()
{
Expand Down
Loading