From 20bfd1c554d8cfa4d629bc28bec9e0790f89ead7 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 24 Jun 2026 11:30:37 +0200 Subject: [PATCH 01/27] fixed debug symbol upload project name --- test/Scripts.Integration.Test/Scripts/CliConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs b/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs index 94ebe9e17..89b72cd1f 100644 --- a/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs @@ -14,7 +14,7 @@ public override void Configure(SentryCliOptions cliOptions) cliOptions.Auth = authToken; cliOptions.Organization = "sentry-sdks"; - cliOptions.Project = "sentry-unity"; + cliOptions.Project = "sentry-unity-integration-tests"; Debug.Log("Sentry: CliConfiguration::Configure() finished"); } From 360e3cfa5c2c129f907fe71e7882f7a873066f8c Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 8 Jun 2026 20:25:30 +0200 Subject: [PATCH 02/27] Add app-hang heartbeat coroutine to SentryMonoBehaviour --- .../SentryMonoBehaviour.AppHang.cs | 41 ++++++++++++++ .../SentryMonoBehaviourAppHangTests.cs | 54 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs create mode 100644 test/Sentry.Unity.Tests/SentryMonoBehaviourAppHangTests.cs diff --git a/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs b/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs new file mode 100644 index 000000000..a0d69e890 --- /dev/null +++ b/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections; +using UnityEngine; + +namespace Sentry.Unity; + +/// +/// Drives the periodic heartbeat used by the native SDK's app-hang detection. +/// The coroutine runs on the Unity main thread, which is the thread the native +/// daemon latches onto as the monitored target (first caller wins). +/// +public partial class SentryMonoBehaviour +{ + private static readonly TimeSpan AppHangHeartbeatInterval = TimeSpan.FromSeconds(1); + + /// + /// Starts the app-hang heartbeat on the main thread at a fixed 1-second interval. + /// + public Coroutine StartAppHangHeartbeat(Action heartbeat) => + StartAppHangHeartbeat(heartbeat, AppHangHeartbeatInterval); + + // Internal overload so tests can use a short interval. + internal Coroutine StartAppHangHeartbeat(Action heartbeat, TimeSpan interval) => + StartCoroutine(AppHangHeartbeatCoroutine(heartbeat, interval)); + + private IEnumerator AppHangHeartbeatCoroutine(Action heartbeat, TimeSpan interval) + { + // Fire immediately to latch the main thread as the monitored target + // before any real hang can occur. + heartbeat(); + + // WaitForSecondsRealtime so a paused or Time.timeScale == 0 game keeps + // heartbeating. + var wait = new WaitForSecondsRealtime((float)interval.TotalSeconds); + while (true) + { + yield return wait; + heartbeat(); + } + } +} diff --git a/test/Sentry.Unity.Tests/SentryMonoBehaviourAppHangTests.cs b/test/Sentry.Unity.Tests/SentryMonoBehaviourAppHangTests.cs new file mode 100644 index 000000000..cbc3d8733 --- /dev/null +++ b/test/Sentry.Unity.Tests/SentryMonoBehaviourAppHangTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections; +using NUnit.Framework; +using Sentry.Unity.Tests.Stubs; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Sentry.Unity.Tests; + +public class SentryMonoBehaviourAppHangTests +{ + private SentryMonoBehaviour GetSut() + { + var gameObject = new GameObject("AppHangTest"); + var sut = gameObject.AddComponent(); + sut.Application = new TestApplication(); + return sut; + } + + [UnityTest] + public IEnumerator StartAppHangHeartbeat_FiresImmediatelyThenPeriodically() + { + var sut = GetSut(); + var count = 0; + + // Use a tiny interval so the test runs fast. + sut.StartAppHangHeartbeat(() => count++, TimeSpan.FromSeconds(0.05)); + + // First heartbeat fires synchronously on start. + Assert.AreEqual(1, count); + + // After roughly two intervals we expect at least two more. + yield return new WaitForSecondsRealtime(0.12f); + + Assert.GreaterOrEqual(count, 3); + } + + [UnityTest] + public IEnumerator StartAppHangHeartbeat_StopsWhenObjectDestroyed() + { + var sut = GetSut(); + var count = 0; + + sut.StartAppHangHeartbeat(() => count++, TimeSpan.FromSeconds(0.05)); + Assert.AreEqual(1, count); + + UnityEngine.Object.DestroyImmediate(sut.gameObject); + var countAfterDestroy = count; + + yield return new WaitForSecondsRealtime(0.12f); + + Assert.AreEqual(countAfterDestroy, count); + } +} From c9146d58467bdba52e8dfb3f0065c353466bbadb Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 8 Jun 2026 20:37:07 +0200 Subject: [PATCH 03/27] Forward app-hang options to sentry-native --- src/Sentry.Unity.Native/SentryNativeBridge.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Sentry.Unity.Native/SentryNativeBridge.cs b/src/Sentry.Unity.Native/SentryNativeBridge.cs index 26f331eea..49c317b59 100644 --- a/src/Sentry.Unity.Native/SentryNativeBridge.cs +++ b/src/Sentry.Unity.Native/SentryNativeBridge.cs @@ -121,6 +121,13 @@ is RuntimePlatform.WindowsPlayer or RuntimePlatform.WindowsServer Logger?.LogInfo("Passing the native logs back to the C# layer is not supported on Mono - skipping native logger."); } + Logger?.LogDebug("Setting EnableAppHangTracking: {0}", options.EnableAppHangTracking); + sentry_options_set_app_hang_enabled(cOptions, options.EnableAppHangTracking ? 1 : 0); + + var appHangTimeoutMs = (ulong)Math.Max(0, options.AppHangTimeout.TotalMilliseconds); + Logger?.LogDebug("Setting AppHangTimeout: {0}ms", appHangTimeoutMs); + sentry_options_set_app_hang_timeout_ms(cOptions, appHangTimeoutMs); + Logger?.LogDebug("Initializing sentry native"); return 0 == sentry_init(cOptions); } @@ -151,6 +158,8 @@ internal static string GetDatabasePath(SentryUnityOptions options, IApplication? internal static void ReinstallBackend() => sentry_reinstall_backend(); + internal static void AppHangHeartbeat() => sentry_app_hang_heartbeat(); + // libsentry.so [DllImport(SentryLib)] private static extern IntPtr sentry_options_new(); @@ -193,6 +202,12 @@ internal static string GetDatabasePath(SentryUnityOptions options, IApplication? [DllImport(SentryLib)] private static extern void sentry_options_set_enable_metrics(IntPtr options, int enable_metrics); + [DllImport(SentryLib)] + private static extern void sentry_options_set_app_hang_enabled(IntPtr options, int enabled); + + [DllImport(SentryLib)] + private static extern void sentry_options_set_app_hang_timeout_ms(IntPtr options, ulong timeout_ms); + [UnmanagedFunctionPointer(CallingConvention.Cdecl, SetLastError = true)] private delegate void sentry_logger_function_t(int level, IntPtr message, IntPtr argsAddress, IntPtr userData); @@ -354,4 +369,7 @@ private static void WithMarshalledStruct(T structure, Action action) [DllImport(SentryLib)] private static extern void sentry_reinstall_backend(); + + [DllImport(SentryLib)] + private static extern void sentry_app_hang_heartbeat(); } From c94b7c65767defcd1b05740a515d6ea0537c4604 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 8 Jun 2026 20:40:11 +0200 Subject: [PATCH 04/27] Start app-hang heartbeat coroutine on native init --- src/Sentry.Unity.Native/SentryNative.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Sentry.Unity.Native/SentryNative.cs b/src/Sentry.Unity.Native/SentryNative.cs index 0ccade4cc..fab184aaf 100644 --- a/src/Sentry.Unity.Native/SentryNative.cs +++ b/src/Sentry.Unity.Native/SentryNative.cs @@ -87,6 +87,12 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf } options.CrashedLastRun = () => crashedLastRun; + if (options.EnableAppHangTracking) + { + Logger?.LogDebug("Starting the app-hang heartbeat coroutine."); + SentryMonoBehaviour.Instance.StartAppHangHeartbeat(SentryNativeBridge.AppHangHeartbeat); + } + ShouldReinstallBackend = true; } From 74d6a3688799981532def2d59e53dffd62ece30d Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 8 Jun 2026 20:42:23 +0200 Subject: [PATCH 05/27] Mention macOS in app-hang tracking tooltip --- src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs b/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs index 489f52bcc..bc602427f 100644 --- a/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs +++ b/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs @@ -64,7 +64,7 @@ internal static void Display(ScriptableSentryUnityOptions options, SentryCliOpti options.EnableAppHangTracking = EditorGUILayout.Toggle( new GUIContent("Enable", - "Enables app hang detection via the native SDK. Currently effective on iOS only; " + + "Enables app hang detection via the native SDK. Effective on iOS, and on macOS when using the Native backend; " + "no-op on other platforms until each platform's native hang detection lands."), options.EnableAppHangTracking); From eab0f31ff23c7f410dc5738aa45aef079c81058e Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 9 Jun 2026 00:40:49 +0200 Subject: [PATCH 06/27] Disable C# ANR watchdog on macOS when native app-hang tracking is enabled --- src/Sentry.Unity.Native/SentryNative.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Sentry.Unity.Native/SentryNative.cs b/src/Sentry.Unity.Native/SentryNative.cs index fab184aaf..3ab983b39 100644 --- a/src/Sentry.Unity.Native/SentryNative.cs +++ b/src/Sentry.Unity.Native/SentryNative.cs @@ -91,6 +91,14 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf { Logger?.LogDebug("Starting the app-hang heartbeat coroutine."); SentryMonoBehaviour.Instance.StartAppHangHeartbeat(SentryNativeBridge.AppHangHeartbeat); + + // sentry-native's app-hang detection is currently macOS-only. Where it is effective, skip the + // C# ANR watchdog so a hang isn't reported twice (mirrors the iOS/sentry-cocoa behavior). + if (platform is RuntimePlatform.OSXPlayer or RuntimePlatform.OSXServer) + { + Logger?.LogDebug("Disabling the C# ANR watchdog - sentry-native handles app hang detection on macOS."); + options.DisableAnrIntegration(); + } } ShouldReinstallBackend = true; From 24b23bd113cb46ec36b18ae7c0f525906e1a40eb Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 9 Jun 2026 10:50:34 +0200 Subject: [PATCH 07/27] Bump sentry-native to include the app hang feature --- modules/sentry-native | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/sentry-native b/modules/sentry-native index ae55c1bc2..68fec63c8 160000 --- a/modules/sentry-native +++ b/modules/sentry-native @@ -1 +1 @@ -Subproject commit ae55c1bc2f828da06b4a88f5d6b23a49e3f45a22 +Subproject commit 68fec63c89240a415455a2f6d9337e9e49373389 From 1956fe4e88f9f3bacb79d7219ef8123c9143421d Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 9 Jun 2026 16:56:36 +0200 Subject: [PATCH 08/27] tweak --- src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs | 14 +++++++++++--- .../SentryMonoBehaviourAppHangTests.cs | 15 +++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs b/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs index a0d69e890..1fed583a8 100644 --- a/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs +++ b/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs @@ -14,7 +14,9 @@ public partial class SentryMonoBehaviour private static readonly TimeSpan AppHangHeartbeatInterval = TimeSpan.FromSeconds(1); /// - /// Starts the app-hang heartbeat on the main thread at a fixed 1-second interval. + /// Starts the app-hang heartbeat on the main thread at a fixed 1-second interval. Arming is + /// deferred until the player loop is running (see ) so + /// the synchronous startup stall isn't reported as a hang. /// public Coroutine StartAppHangHeartbeat(Action heartbeat) => StartAppHangHeartbeat(heartbeat, AppHangHeartbeatInterval); @@ -25,8 +27,14 @@ internal Coroutine StartAppHangHeartbeat(Action heartbeat, TimeSpan interval) => private IEnumerator AppHangHeartbeatCoroutine(Action heartbeat, TimeSpan interval) { - // Fire immediately to latch the main thread as the monitored target - // before any real hang can occur. + // Defer arming by a frame. The first heartbeat both latches the main thread as the + // monitored target and arms detection (the native side treats a missing heartbeat as "not + // armed"). Startup - splash screen plus the first scene load - routinely blocks the main + // thread longer than the hang timeout, so arming any earlier reports that startup stall as + // a false hang. A single 'yield return null' suspends until the player loop ticks, by which + // point the synchronous startup work is behind us. Unlike WaitForEndOfFrame this also + // resumes in batchmode/headless (e.g. OSXServer), so detection still arms there. + yield return null; heartbeat(); // WaitForSecondsRealtime so a paused or Time.timeScale == 0 game keeps diff --git a/test/Sentry.Unity.Tests/SentryMonoBehaviourAppHangTests.cs b/test/Sentry.Unity.Tests/SentryMonoBehaviourAppHangTests.cs index cbc3d8733..a96edefaf 100644 --- a/test/Sentry.Unity.Tests/SentryMonoBehaviourAppHangTests.cs +++ b/test/Sentry.Unity.Tests/SentryMonoBehaviourAppHangTests.cs @@ -18,7 +18,7 @@ private SentryMonoBehaviour GetSut() } [UnityTest] - public IEnumerator StartAppHangHeartbeat_FiresImmediatelyThenPeriodically() + public IEnumerator StartAppHangHeartbeat_ArmsAfterFirstFrameThenFiresPeriodically() { var sut = GetSut(); var count = 0; @@ -26,8 +26,12 @@ public IEnumerator StartAppHangHeartbeat_FiresImmediatelyThenPeriodically() // Use a tiny interval so the test runs fast. sut.StartAppHangHeartbeat(() => count++, TimeSpan.FromSeconds(0.05)); - // First heartbeat fires synchronously on start. - Assert.AreEqual(1, count); + // Arming is deferred until the player loop ticks, so nothing fires synchronously on start. + Assert.AreEqual(0, count); + + // The first heartbeat fires once a frame has passed. + yield return null; + Assert.GreaterOrEqual(count, 1); // After roughly two intervals we expect at least two more. yield return new WaitForSecondsRealtime(0.12f); @@ -42,7 +46,10 @@ public IEnumerator StartAppHangHeartbeat_StopsWhenObjectDestroyed() var count = 0; sut.StartAppHangHeartbeat(() => count++, TimeSpan.FromSeconds(0.05)); - Assert.AreEqual(1, count); + + // Let it arm (deferred until the player loop ticks). + yield return null; + Assert.GreaterOrEqual(count, 1); UnityEngine.Object.DestroyImmediate(sut.gameObject); var countAfterDestroy = count; From ecdb6bede6304772e2a9f8eabda62bf037d38ee2 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 10 Jun 2026 16:10:35 +0200 Subject: [PATCH 09/27] bumped sentry-native, targeting desktop --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea2f3ac29..ecbd1b3b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,11 @@ - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#351) - [diff](https://github.com/getsentry/sentry-cli/compare/3.5.0...3.5.1) +## Unreleased + +- Extended `EnableAppHangTracking` (default `true`) and `AppHangTimeout` (default `5s`) to enable app hang detection via the native SDK on macOS, Windows, and Linux. This works through `sentry-native` when using the Native backend. This requires the backend to be switched to `native` in the Advanced -> Experimental settings. The native SDK monitors the main thread and produces a stack trace for the hang event. When enabled, the Unity SDK's C# watchdog is skipped to avoid duplicate reports ([#2679](https://github.com/getsentry/sentry-unity/pull/2679)) + + ## 4.4.0 ### Features From 3a715d3946c4b732f0d484c79dee1c69e451fd23 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 11 Jun 2026 11:29:44 +0200 Subject: [PATCH 10/27] tooltips and logs --- .../ConfigurationWindow/AdvancedTab.cs | 2 +- src/Sentry.Unity.Native/SentryNative.cs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs b/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs index bc602427f..344896c15 100644 --- a/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs +++ b/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs @@ -64,7 +64,7 @@ internal static void Display(ScriptableSentryUnityOptions options, SentryCliOpti options.EnableAppHangTracking = EditorGUILayout.Toggle( new GUIContent("Enable", - "Enables app hang detection via the native SDK. Effective on iOS, and on macOS when using the Native backend; " + + "Enables app hang detection via the native SDK. Effective on iOS, and on macOS, Windows, and Linux when using the Native backend; " + "no-op on other platforms until each platform's native hang detection lands."), options.EnableAppHangTracking); diff --git a/src/Sentry.Unity.Native/SentryNative.cs b/src/Sentry.Unity.Native/SentryNative.cs index 3ab983b39..335295266 100644 --- a/src/Sentry.Unity.Native/SentryNative.cs +++ b/src/Sentry.Unity.Native/SentryNative.cs @@ -92,11 +92,13 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf Logger?.LogDebug("Starting the app-hang heartbeat coroutine."); SentryMonoBehaviour.Instance.StartAppHangHeartbeat(SentryNativeBridge.AppHangHeartbeat); - // sentry-native's app-hang detection is currently macOS-only. Where it is effective, skip the + // sentry-native handles app-hang detection on the desktop platforms. Where it is effective, skip the // C# ANR watchdog so a hang isn't reported twice (mirrors the iOS/sentry-cocoa behavior). - if (platform is RuntimePlatform.OSXPlayer or RuntimePlatform.OSXServer) + if (platform is RuntimePlatform.OSXPlayer or RuntimePlatform.OSXServer + or RuntimePlatform.WindowsPlayer or RuntimePlatform.WindowsServer + or RuntimePlatform.LinuxPlayer or RuntimePlatform.LinuxServer) { - Logger?.LogDebug("Disabling the C# ANR watchdog - sentry-native handles app hang detection on macOS."); + Logger?.LogDebug("Disabling the C# ANR watchdog - sentry-native handles app hang detection."); options.DisableAnrIntegration(); } } From 8350c0b5542154e806f1dcf0ac015a0a17ae7f2e Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 19 Jun 2026 17:02:29 +0200 Subject: [PATCH 11/27] app hangs in integration test --- test/IntegrationTest/Integration.Tests.ps1 | 50 +++++++++++++++++++ .../IntegrationOptionsConfiguration.cs | 6 +++ .../Scripts/IntegrationTester.cs | 37 ++++++++++++++ 3 files changed, 93 insertions(+) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index d4c3048c9..1553fe320 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -408,3 +408,53 @@ if ($env:SENTRY_TEST_PLATFORM -ne "WebGL") { } } } + +# Native in-proc app-hang detection runs through sentry-native. Under the current test config it is +# active on Windows (Crashpad) and Linux; macOS uses the Cocoa backend (sentry-cocoa app-hang path), +# so it is excluded here along with the non-desktop platforms. +if ($env:SENTRY_TEST_PLATFORM -eq "Desktop" -and ($IsWindows -or $IsLinux)) { + Describe "Unity $($env:SENTRY_TEST_PLATFORM) App Hang Tests" { + + Context "App Hang Capture" { + BeforeAll { + $script:runEvent = $null + $script:runResult = Invoke-TestAction -Action "app-hang-capture" + + # The native app-hang event is captured in-proc (same run, no relaunch). Its event ID + # is generated natively, so look it up by the unique scope tag the app sets instead. + $hangId = Get-EventIds -AppOutput $script:runResult.Output -ExpectedCount 1 + if ($hangId) { + Write-Host "::group::Getting event content" + $script:runEvent = Get-SentryTestEvent -TagName "test.app_hang_id" -TagValue "$hangId" -TimeoutSeconds 300 + Write-Host "::endgroup::" + } + } + + It "" -ForEach $CommonTestCases { + & $testBlock -SentryEvent $runEvent -TestType "app-hang-capture" -RunResult $runResult -TestSetup $script:TestSetup + } + + It "Has error level" { + ($runEvent.tags | Where-Object { $_.key -eq "level" }).value | Should -Be "error" + } + + It "Has AppHang exception with stacktrace" { + $runEvent.exception | Should -Not -BeNullOrEmpty + $runEvent.exception.values | Should -Not -BeNullOrEmpty + $exception = $runEvent.exception.values[0] + $exception | Should -Not -BeNullOrEmpty + $exception.type | Should -Be "AppHang" + $exception.value | Should -Match "App hung for at least" + $exception.stacktrace | Should -Not -BeNullOrEmpty + } + + It "Has AppHang mechanism" { + $mechanism = $runEvent.exception.values[0].mechanism + $mechanism | Should -Not -BeNullOrEmpty + $mechanism.type | Should -Be "AppHang" + $mechanism.handled | Should -Be $true + $mechanism.synthetic | Should -Be $true + } + } + } +} diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs index c3f98c136..47cc04f57 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs @@ -41,6 +41,12 @@ public override void Configure(SentryUnityOptions options) // Disable ANR to avoid test interference options.DisableAnrIntegration(); + // Enable native in-proc app-hang detection and shorten its timeout so the app-hang-capture + // test blocks for less time. Safe to lower globally: no other test action blocks the main + // thread and heartbeat arming is deferred past startup. + options.EnableAppHangTracking = true; + options.AppHangTimeout = TimeSpan.FromSeconds(2); + // Runtime initialization for integration tests options.AndroidNativeInitializationType = NativeInitializationType.Runtime; options.IosNativeInitializationType = NativeInitializationType.Runtime; diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs index 2cfca4339..8563b6ea5 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs @@ -40,6 +40,9 @@ public void Start() case "crash-capture": StartCoroutine(CrashCapture()); break; + case "app-hang-capture": + StartCoroutine(AppHangCapture()); + break; case "crash-send": CrashSend(); break; @@ -188,6 +191,40 @@ private IEnumerator CrashCapture() Application.Quit(1); } + private IEnumerator AppHangCapture() + { + var hangId = Guid.NewGuid().ToString(); + + AddIntegrationTestContext("app-hang-capture"); + + // The native app-hang event is captured in-proc by sentry-native and its event ID is not + // visible to C#. Tag the scope with a unique ID so the test harness can look the event up, + // the same way crash-capture does (scope tags sync to the native layer). + SentrySdk.ConfigureScope(scope => + { + scope.SetTag("test.app_hang_id", hangId); + }); + + // Wait for the scope sync to complete and for the app-hang heartbeat coroutine to arm + // (arming is deliberately deferred by a frame so startup isn't reported as a hang). + yield return new WaitForSeconds(0.5f); + + Logger.Log($"EVENT_CAPTURED: {hangId}"); + Logger.Log("APP HANG TEST: Blocking the main thread to trigger native app-hang detection"); + + // Block the main thread well past AppHangTimeout (2s in IntegrationOptionsConfiguration), + // clearing the watchdog's 500ms poll and 1s heartbeat interval so detection reliably fires. + System.Threading.Thread.Sleep(5000); + + // The main thread is responsive again; let the heartbeat resume and flush the captured event. + yield return null; + + SentrySdk.FlushAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult(); + + Logger.Log("APP HANG TEST: Flush complete, quitting."); + Application.Quit(0); + } + private void CrashSend() { Logger.Log("CrashSend: Initializing Sentry to flush cached crash report..."); From c1ca5979cb52322495cc5769576631b285b463f2 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 22 Jun 2026 12:18:33 +0200 Subject: [PATCH 12/27] updated changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecbd1b3b8..a74abb87f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,8 +28,7 @@ ## Unreleased -- Extended `EnableAppHangTracking` (default `true`) and `AppHangTimeout` (default `5s`) to enable app hang detection via the native SDK on macOS, Windows, and Linux. This works through `sentry-native` when using the Native backend. This requires the backend to be switched to `native` in the Advanced -> Experimental settings. The native SDK monitors the main thread and produces a stack trace for the hang event. When enabled, the Unity SDK's C# watchdog is skipped to avoid duplicate reports ([#2679](https://github.com/getsentry/sentry-unity/pull/2679)) - +- Extended `EnableAppHangTracking` (default `true`) and `AppHangTimeout` (default `5s`) to enable app hang detection via `sentry-native` on macOS, Windows, and Linux. For macOS this requires the backend to be switched to `native` in the Advanced -> Experimental settings. `sentry-native` monitors the main thread and produces a stack trace for the hang event. When enabled, the Unity SDK's C# watchdog is skipped to avoid duplicate reports. ([#2679](https://github.com/getsentry/sentry-unity/pull/2679)) ## 4.4.0 From 38d98a785f8053df152d4b06afd7d125ea4a3763 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 22 Jun 2026 13:06:01 +0200 Subject: [PATCH 13/27] run app hang test on macos with sentry-native --- .github/workflows/test-run-desktop.yml | 2 ++ test/IntegrationTest/Integration.Tests.ps1 | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-run-desktop.yml b/.github/workflows/test-run-desktop.yml index 881c8b532..fd7f3243a 100644 --- a/.github/workflows/test-run-desktop.yml +++ b/.github/workflows/test-run-desktop.yml @@ -66,6 +66,8 @@ jobs: - name: Run Integration Tests (macOS) if: inputs.platform == 'macos' timeout-minutes: 20 + env: + SENTRY_TEST_BACKEND: ${{ inputs.backend }} run: | $env:SENTRY_TEST_PLATFORM = "Desktop" $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.app/Contents/MacOS/IntegrationTest" diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index 1553fe320..b0b475470 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -409,10 +409,12 @@ if ($env:SENTRY_TEST_PLATFORM -ne "WebGL") { } } -# Native in-proc app-hang detection runs through sentry-native. Under the current test config it is -# active on Windows (Crashpad) and Linux; macOS uses the Cocoa backend (sentry-cocoa app-hang path), -# so it is excluded here along with the non-desktop platforms. -if ($env:SENTRY_TEST_PLATFORM -eq "Desktop" -and ($IsWindows -or $IsLinux)) { +# In-proc app-hang detection is driven by the C# heartbeat coroutine that pings sentry-native, so it +# is active for every desktop configuration that uses the sentry-native code path. The exception +# is macOS with the Cocoa backend: there the SDK uses sentry-cocoa, which has its own app-hang path +# and never starts the in-proc heartbeat, so skip that one configuration. +$isCocoaBackend = $env:SENTRY_TEST_BACKEND -eq "cocoa" +if ($env:SENTRY_TEST_PLATFORM -eq "Desktop" -and -not $isCocoaBackend) { Describe "Unity $($env:SENTRY_TEST_PLATFORM) App Hang Tests" { Context "App Hang Capture" { From 7a8d8c7dd970ae8754805359b4cb34f74b258c95 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 22 Jun 2026 15:15:29 +0200 Subject: [PATCH 14/27] comments --- .../ConfigurationWindow/AdvancedTab.cs | 2 +- .../SentryMonoBehaviour.AppHang.cs | 19 ++++++++----------- .../IntegrationOptionsConfiguration.cs | 4 +--- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs b/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs index 344896c15..32f74a3a2 100644 --- a/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs +++ b/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs @@ -64,7 +64,7 @@ internal static void Display(ScriptableSentryUnityOptions options, SentryCliOpti options.EnableAppHangTracking = EditorGUILayout.Toggle( new GUIContent("Enable", - "Enables app hang detection via the native SDK. Effective on iOS, and on macOS, Windows, and Linux when using the Native backend; " + + "Enables app hang detection via the native SDK. Effective on iOS, and on macOS, Windows, and Linux when using sentry-native; " + "no-op on other platforms until each platform's native hang detection lands."), options.EnableAppHangTracking); diff --git a/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs b/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs index 1fed583a8..931d80c82 100644 --- a/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs +++ b/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs @@ -5,9 +5,9 @@ namespace Sentry.Unity; /// -/// Drives the periodic heartbeat used by the native SDK's app-hang detection. +/// Drives the periodic heartbeat used by sentry-native's app-hang detection. /// The coroutine runs on the Unity main thread, which is the thread the native -/// daemon latches onto as the monitored target (first caller wins). +/// daemon latches onto as the monitored target. /// public partial class SentryMonoBehaviour { @@ -27,18 +27,15 @@ internal Coroutine StartAppHangHeartbeat(Action heartbeat, TimeSpan interval) => private IEnumerator AppHangHeartbeatCoroutine(Action heartbeat, TimeSpan interval) { - // Defer arming by a frame. The first heartbeat both latches the main thread as the - // monitored target and arms detection (the native side treats a missing heartbeat as "not - // armed"). Startup - splash screen plus the first scene load - routinely blocks the main - // thread longer than the hang timeout, so arming any earlier reports that startup stall as - // a false hang. A single 'yield return null' suspends until the player loop ticks, by which - // point the synchronous startup work is behind us. Unlike WaitForEndOfFrame this also - // resumes in batchmode/headless (e.g. OSXServer), so detection still arms there. + // Skipping the first frame. The first heartbeat both latches the main thread as the + // monitored target and arms detection. The monitor no-op without having received a + // heartbeat. During startup, splash screen plus the first scene load routinely block the + // main thread longer than the hang timeout and would cause false positives. + // This also works in batchmode/headless (e.g. LinuxServer). + // (unlike WaitForEndOfFrame) yield return null; heartbeat(); - // WaitForSecondsRealtime so a paused or Time.timeScale == 0 game keeps - // heartbeating. var wait = new WaitForSecondsRealtime((float)interval.TotalSeconds); while (true) { diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs index 47cc04f57..7cd31de35 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs @@ -41,9 +41,7 @@ public override void Configure(SentryUnityOptions options) // Disable ANR to avoid test interference options.DisableAnrIntegration(); - // Enable native in-proc app-hang detection and shorten its timeout so the app-hang-capture - // test blocks for less time. Safe to lower globally: no other test action blocks the main - // thread and heartbeat arming is deferred past startup. + // App-Hang detection options.EnableAppHangTracking = true; options.AppHangTimeout = TimeSpan.FromSeconds(2); From 9a29a0433ce95dd8d6e4b11016f750e20bcff36d Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 22 Jun 2026 15:15:36 +0200 Subject: [PATCH 15/27] reverted changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a74abb87f..e21326d07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 4.5.0 +## Unreleased + ### Fixes - The SDK now also syncs breadcrumb data to the native layer so they are available on events coming from the native SDKs ([#2720](https://github.com/getsentry/sentry-unity/pull/2720)) From c9450bd0d293345260aff542d51ca8ae62b21fa3 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 22 Jun 2026 15:16:54 +0200 Subject: [PATCH 16/27] updated changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e21326d07..a445bb6c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Extended `EnableAppHangTracking` (default `true`) and `AppHangTimeout` (default `5s`) to enable app hang detection via `sentry-native` on macOS, Windows, and Linux. For macOS this requires the backend to be switched to `native` in the Advanced -> Experimental settings. `sentry-native` monitors the main thread and produces a stack trace for the hang event. When enabled, the Unity SDK's C# watchdog is skipped to avoid duplicate reports. ([#2679](https://github.com/getsentry/sentry-unity/pull/2679)) + ## 4.5.0 ## Unreleased From f607f51faefc603918f3834c68ae8e5f03ded461 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 22 Jun 2026 16:23:21 +0200 Subject: [PATCH 17/27] made native app hang tracking experimental --- CHANGELOG.md | 2 +- .../ConfigurationWindow/AdvancedTab.cs | 15 ++++++++++++--- src/Sentry.Unity.Native/SentryNative.cs | 2 +- src/Sentry.Unity.Native/SentryNativeBridge.cs | 4 ++-- .../ExperimentalSentryUnityOptions.cs | 9 +++++++++ src/Sentry.Unity/ScriptableSentryUnityOptions.cs | 1 + src/Sentry.Unity/SentryUnityOptions.cs | 6 +++--- .../Scripts/IntegrationOptionsConfiguration.cs | 4 +++- .../Sentry.Unity.Tests/SentryUnityOptionsTests.cs | 15 +++++++++++++++ 9 files changed, 47 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a445bb6c0..d3c154b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Extended `EnableAppHangTracking` (default `true`) and `AppHangTimeout` (default `5s`) to enable app hang detection via `sentry-native` on macOS, Windows, and Linux. For macOS this requires the backend to be switched to `native` in the Advanced -> Experimental settings. `sentry-native` monitors the main thread and produces a stack trace for the hang event. When enabled, the Unity SDK's C# watchdog is skipped to avoid duplicate reports. ([#2679](https://github.com/getsentry/sentry-unity/pull/2679)) +- Added the experimental `options.Experimental.EnableNativeAppHangTracking` (default `false`) to enable app hang detection via `sentry-native` on macOS, Windows, and Linux. This requires the corresponding platform backend to be switched to `Native` in the Advanced -> Experimental settings. `sentry-native` monitors the main thread and produces a stack trace for the hang event, reusing the top-level `AppHangTimeout` (default `5s`). When effective, the Unity SDK's C# watchdog is skipped to avoid duplicate reports. iOS app hang detection remains controlled by the top-level `EnableAppHangTracking`. ([#2709](https://github.com/getsentry/sentry-unity/pull/2709)) ## 4.5.0 diff --git a/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs b/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs index 32f74a3a2..9b8b078c7 100644 --- a/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs +++ b/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs @@ -64,14 +64,15 @@ internal static void Display(ScriptableSentryUnityOptions options, SentryCliOpti options.EnableAppHangTracking = EditorGUILayout.Toggle( new GUIContent("Enable", - "Enables app hang detection via the native SDK. Effective on iOS, and on macOS, Windows, and Linux when using sentry-native; " + - "no-op on other platforms until each platform's native hang detection lands."), + "Enables app hang detection on iOS via sentry-cocoa. App hang detection on macOS, " + + "Windows, and Linux is experimental and controlled separately in the Experimental section."), options.EnableAppHangTracking); options.AppHangTimeout = EditorGUILayout.IntField( new GUIContent("App Hang Timeout [ms]", "The duration in [ms] for how long the main thread has to be blocked " + - "before an app hang is reported.\nDefault: 5000ms"), + "before an app hang is reported. Shared with the experimental native app hang " + + "detection.\nDefault: 5000ms"), options.AppHangTimeout); options.AppHangTimeout = Math.Max(0, options.AppHangTimeout); } @@ -270,6 +271,14 @@ internal static void Display(ScriptableSentryUnityOptions options, SentryCliOpti "Uploads crashes immediately."), options.Experimental.LinuxBackend); } + + options.Experimental.EnableNativeAppHangTracking = EditorGUILayout.Toggle( + new GUIContent( + "Native App Hang Tracking", + "Enables app hang detection via sentry-native on macOS, Windows, and Linux. Requires the " + + "corresponding platform backend above to be set to 'Native'. Shares the App Hang Timeout " + + "configured in the App Hang Tracking section. iOS is unaffected by this option."), + options.Experimental.EnableNativeAppHangTracking); } EditorGUI.indentLevel--; EditorGUILayout.EndFoldoutHeaderGroup(); diff --git a/src/Sentry.Unity.Native/SentryNative.cs b/src/Sentry.Unity.Native/SentryNative.cs index 335295266..3ea623010 100644 --- a/src/Sentry.Unity.Native/SentryNative.cs +++ b/src/Sentry.Unity.Native/SentryNative.cs @@ -87,7 +87,7 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf } options.CrashedLastRun = () => crashedLastRun; - if (options.EnableAppHangTracking) + if (options.Experimental.EnableNativeAppHangTracking) { Logger?.LogDebug("Starting the app-hang heartbeat coroutine."); SentryMonoBehaviour.Instance.StartAppHangHeartbeat(SentryNativeBridge.AppHangHeartbeat); diff --git a/src/Sentry.Unity.Native/SentryNativeBridge.cs b/src/Sentry.Unity.Native/SentryNativeBridge.cs index 49c317b59..330d0672b 100644 --- a/src/Sentry.Unity.Native/SentryNativeBridge.cs +++ b/src/Sentry.Unity.Native/SentryNativeBridge.cs @@ -121,8 +121,8 @@ is RuntimePlatform.WindowsPlayer or RuntimePlatform.WindowsServer Logger?.LogInfo("Passing the native logs back to the C# layer is not supported on Mono - skipping native logger."); } - Logger?.LogDebug("Setting EnableAppHangTracking: {0}", options.EnableAppHangTracking); - sentry_options_set_app_hang_enabled(cOptions, options.EnableAppHangTracking ? 1 : 0); + Logger?.LogDebug("Setting EnableNativeAppHangTracking: {0}", options.Experimental.EnableNativeAppHangTracking); + sentry_options_set_app_hang_enabled(cOptions, options.Experimental.EnableNativeAppHangTracking ? 1 : 0); var appHangTimeoutMs = (ulong)Math.Max(0, options.AppHangTimeout.TotalMilliseconds); Logger?.LogDebug("Setting AppHangTimeout: {0}ms", appHangTimeoutMs); diff --git a/src/Sentry.Unity/ExperimentalSentryUnityOptions.cs b/src/Sentry.Unity/ExperimentalSentryUnityOptions.cs index b05e34d21..bd03043e8 100644 --- a/src/Sentry.Unity/ExperimentalSentryUnityOptions.cs +++ b/src/Sentry.Unity/ExperimentalSentryUnityOptions.cs @@ -38,4 +38,13 @@ public class ExperimentalSentryUnityOptions /// a minimum of 10 seconds so the out-of-process handler has time to flush before the player exits. /// [field: SerializeField] public LinuxBackend LinuxBackend { get; set; } = LinuxBackend.Breakpad; + + /// + /// Enables app hang detection via sentry-native on macOS, Windows, and Linux. Defaults to + /// false. Requires the backend to be switched to + /// on macOS. sentry-native monitors the main thread and + /// produces an app hang event including a stack trace. When enabled, the C# watchdog is skipped to avoid + /// duplicate reports. The timeout is taken from AppHangTimeout. + /// + [field: SerializeField] public bool EnableNativeAppHangTracking { get; set; } = false; } diff --git a/src/Sentry.Unity/ScriptableSentryUnityOptions.cs b/src/Sentry.Unity/ScriptableSentryUnityOptions.cs index 168b8f565..1841c1a65 100644 --- a/src/Sentry.Unity/ScriptableSentryUnityOptions.cs +++ b/src/Sentry.Unity/ScriptableSentryUnityOptions.cs @@ -237,6 +237,7 @@ internal SentryUnityOptions ToSentryUnityOptions( options.Experimental.MacosBackend = Experimental.MacosBackend; options.Experimental.WindowsBackend = Experimental.WindowsBackend; options.Experimental.LinuxBackend = Experimental.LinuxBackend; + options.Experimental.EnableNativeAppHangTracking = Experimental.EnableNativeAppHangTracking; // By default, the cacheDirectoryPath gets set on known platforms. We're overwriting this behaviour here. if (!EnableOfflineCaching) diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 512d92b24..07e1dd89c 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -186,9 +186,9 @@ public sealed class SentryUnityOptions : SentryOptions public bool IosWatchdogTerminationIntegrationEnabled { get; set; } = false; /// - /// Enables app hang detection on platforms whose native SDK can deliver Unity-thread hang - /// coverage. Currently effective on iOS only; on other platforms this is a no-op until each - /// platform's native hang detection lands. + /// Enables app hang detection on iOS through sentry-cocoa, which monitors the main thread and + /// produces a stack trace for the hang event. App hang detection on macOS, Windows, and Linux is + /// experimental and controlled separately via Experimental.EnableNativeAppHangTracking. /// public bool EnableAppHangTracking { get; set; } = true; diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs index 7cd31de35..0f96f4558 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs @@ -41,8 +41,10 @@ public override void Configure(SentryUnityOptions options) // Disable ANR to avoid test interference options.DisableAnrIntegration(); - // App-Hang detection + // App-Hang detection. EnableAppHangTracking drives iOS (sentry-cocoa); + // EnableNativeAppHangTracking drives the desktop sentry-native path. Both share AppHangTimeout. options.EnableAppHangTracking = true; + options.Experimental.EnableNativeAppHangTracking = true; options.AppHangTimeout = TimeSpan.FromSeconds(2); // Runtime initialization for integration tests diff --git a/test/Sentry.Unity.Tests/SentryUnityOptionsTests.cs b/test/Sentry.Unity.Tests/SentryUnityOptionsTests.cs index c17f77fdd..75d689b8c 100644 --- a/test/Sentry.Unity.Tests/SentryUnityOptionsTests.cs +++ b/test/Sentry.Unity.Tests/SentryUnityOptionsTests.cs @@ -156,4 +156,19 @@ public void Options_Experimental_LinuxBackend_IsSettable() options.Experimental.LinuxBackend = LinuxBackend.Native; Assert.AreEqual(LinuxBackend.Native, options.Experimental.LinuxBackend); } + + [Test] + public void Options_Experimental_EnableNativeAppHangTracking_DefaultsToFalse() + { + var options = new SentryUnityOptions(); + Assert.IsFalse(options.Experimental.EnableNativeAppHangTracking); + } + + [Test] + public void Options_Experimental_EnableNativeAppHangTracking_IsSettable() + { + var options = new SentryUnityOptions(); + options.Experimental.EnableNativeAppHangTracking = true; + Assert.IsTrue(options.Experimental.EnableNativeAppHangTracking); + } } From dd8b25cb5140fb275b4163cb7adc4233f43d087d Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 23 Jun 2026 09:36:50 +0200 Subject: [PATCH 18/27] fixed bridge method names --- src/Sentry.Unity.Native/SentryNativeBridge.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Sentry.Unity.Native/SentryNativeBridge.cs b/src/Sentry.Unity.Native/SentryNativeBridge.cs index 330d0672b..fae8a5a27 100644 --- a/src/Sentry.Unity.Native/SentryNativeBridge.cs +++ b/src/Sentry.Unity.Native/SentryNativeBridge.cs @@ -122,7 +122,7 @@ is RuntimePlatform.WindowsPlayer or RuntimePlatform.WindowsServer } Logger?.LogDebug("Setting EnableNativeAppHangTracking: {0}", options.Experimental.EnableNativeAppHangTracking); - sentry_options_set_app_hang_enabled(cOptions, options.Experimental.EnableNativeAppHangTracking ? 1 : 0); + sentry_options_set_enable_app_hang_tracking(cOptions, options.Experimental.EnableNativeAppHangTracking ? 1 : 0); var appHangTimeoutMs = (ulong)Math.Max(0, options.AppHangTimeout.TotalMilliseconds); Logger?.LogDebug("Setting AppHangTimeout: {0}ms", appHangTimeoutMs); @@ -203,7 +203,7 @@ internal static string GetDatabasePath(SentryUnityOptions options, IApplication? private static extern void sentry_options_set_enable_metrics(IntPtr options, int enable_metrics); [DllImport(SentryLib)] - private static extern void sentry_options_set_app_hang_enabled(IntPtr options, int enabled); + private static extern void sentry_options_set_enable_app_hang_tracking(IntPtr options, int enabled); [DllImport(SentryLib)] private static extern void sentry_options_set_app_hang_timeout_ms(IntPtr options, ulong timeout_ms); From bc0eeaf7ed2635e44beb8d3bd22faa6993a9b4a3 Mon Sep 17 00:00:00 2001 From: Stefan Jandl Date: Mon, 22 Jun 2026 17:31:48 +0200 Subject: [PATCH 19/27] Apply suggestion from @bitsandfoxes --- .../Scripts/IntegrationOptionsConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs index 0f96f4558..4f909b15c 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs @@ -43,7 +43,7 @@ public override void Configure(SentryUnityOptions options) // App-Hang detection. EnableAppHangTracking drives iOS (sentry-cocoa); // EnableNativeAppHangTracking drives the desktop sentry-native path. Both share AppHangTimeout. - options.EnableAppHangTracking = true; + options.Experimental.EnableNativeAppHangTracking = true; options.Experimental.EnableNativeAppHangTracking = true; options.AppHangTimeout = TimeSpan.FromSeconds(2); From 7355756a93d3d2580a96c81727651429886027a5 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 23 Jun 2026 11:22:53 +0200 Subject: [PATCH 20/27] bumped native --- src/Sentry.Unity.Native/SentryNativeBridge.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Sentry.Unity.Native/SentryNativeBridge.cs b/src/Sentry.Unity.Native/SentryNativeBridge.cs index fae8a5a27..0919895b5 100644 --- a/src/Sentry.Unity.Native/SentryNativeBridge.cs +++ b/src/Sentry.Unity.Native/SentryNativeBridge.cs @@ -126,7 +126,7 @@ is RuntimePlatform.WindowsPlayer or RuntimePlatform.WindowsServer var appHangTimeoutMs = (ulong)Math.Max(0, options.AppHangTimeout.TotalMilliseconds); Logger?.LogDebug("Setting AppHangTimeout: {0}ms", appHangTimeoutMs); - sentry_options_set_app_hang_timeout_ms(cOptions, appHangTimeoutMs); + sentry_options_set_app_hang_timeout(cOptions, appHangTimeoutMs); Logger?.LogDebug("Initializing sentry native"); return 0 == sentry_init(cOptions); @@ -206,7 +206,7 @@ internal static string GetDatabasePath(SentryUnityOptions options, IApplication? private static extern void sentry_options_set_enable_app_hang_tracking(IntPtr options, int enabled); [DllImport(SentryLib)] - private static extern void sentry_options_set_app_hang_timeout_ms(IntPtr options, ulong timeout_ms); + private static extern void sentry_options_set_app_hang_timeout(IntPtr options, ulong timeout); [UnmanagedFunctionPointer(CallingConvention.Cdecl, SetLastError = true)] private delegate void sentry_logger_function_t(int level, IntPtr message, IntPtr argsAddress, IntPtr userData); From 61558cebe68e5381955277630f2e414a89483277 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 24 Jun 2026 14:04:02 +0200 Subject: [PATCH 21/27] updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3c154b11..62dabf077 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Added the experimental `options.Experimental.EnableNativeAppHangTracking` (default `false`) to enable app hang detection via `sentry-native` on macOS, Windows, and Linux. This requires the corresponding platform backend to be switched to `Native` in the Advanced -> Experimental settings. `sentry-native` monitors the main thread and produces a stack trace for the hang event, reusing the top-level `AppHangTimeout` (default `5s`). When effective, the Unity SDK's C# watchdog is skipped to avoid duplicate reports. iOS app hang detection remains controlled by the top-level `EnableAppHangTracking`. ([#2709](https://github.com/getsentry/sentry-unity/pull/2709)) +- Added the experimental `options.Experimental.EnableNativeAppHangTracking` (default `false`) to enable app hang detection via `sentry-native` on macOS, Windows, and Linux. On macOS, this requires the crash reporting backend to be switched to `Native` in the Advanced -> Experimental settings. `sentry-native` monitors the main thread and produces an event including a stack trace for the hang, reusing the top-level `AppHangTimeout` (default `5s`). When effective, the Unity SDK's C# watchdog is skipped to avoid duplicate reports. iOS app hang detection remains controlled by the top-level `EnableAppHangTracking`. ([#2709](https://github.com/getsentry/sentry-unity/pull/2709)) ## 4.5.0 From cfe79c1961cfb1db6c858b55aa89c8ba85bcdb95 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 24 Jun 2026 14:07:28 +0200 Subject: [PATCH 22/27] . --- CHANGELOG.md | 2 -- src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs | 3 +-- src/Sentry.Unity/SentryUnityOptions.cs | 6 +++--- .../Scripts/IntegrationOptionsConfiguration.cs | 4 +--- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62dabf077..067b832ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,6 @@ ## 4.5.0 -## Unreleased - ### Fixes - The SDK now also syncs breadcrumb data to the native layer so they are available on events coming from the native SDKs ([#2720](https://github.com/getsentry/sentry-unity/pull/2720)) diff --git a/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs b/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs index 931d80c82..9ee518233 100644 --- a/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs +++ b/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs @@ -31,8 +31,7 @@ private IEnumerator AppHangHeartbeatCoroutine(Action heartbeat, TimeSpan interva // monitored target and arms detection. The monitor no-op without having received a // heartbeat. During startup, splash screen plus the first scene load routinely block the // main thread longer than the hang timeout and would cause false positives. - // This also works in batchmode/headless (e.g. LinuxServer). - // (unlike WaitForEndOfFrame) + // This also works in batchmode/headless (e.g. LinuxServer), unlike WaitForEndOfFrame. yield return null; heartbeat(); diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 07e1dd89c..50128d4f2 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -186,9 +186,9 @@ public sealed class SentryUnityOptions : SentryOptions public bool IosWatchdogTerminationIntegrationEnabled { get; set; } = false; /// - /// Enables app hang detection on iOS through sentry-cocoa, which monitors the main thread and - /// produces a stack trace for the hang event. App hang detection on macOS, Windows, and Linux is - /// experimental and controlled separately via Experimental.EnableNativeAppHangTracking. + /// Enables app hang detection on iOS through sentry-cocoa, which monitors the main thread. + /// App hang detection on macOS, Windows, and Linux is experimental and controlled separately via + /// Experimental.EnableNativeAppHangTracking. /// public bool EnableAppHangTracking { get; set; } = true; diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs index 4f909b15c..f52dd4944 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs @@ -41,9 +41,7 @@ public override void Configure(SentryUnityOptions options) // Disable ANR to avoid test interference options.DisableAnrIntegration(); - // App-Hang detection. EnableAppHangTracking drives iOS (sentry-cocoa); - // EnableNativeAppHangTracking drives the desktop sentry-native path. Both share AppHangTimeout. - options.Experimental.EnableNativeAppHangTracking = true; + // App Hang tracking options.Experimental.EnableNativeAppHangTracking = true; options.AppHangTimeout = TimeSpan.FromSeconds(2); From f0b150551fbfbe10ffdb90241f7e55f4549f2a9f Mon Sep 17 00:00:00 2001 From: Stefan Jandl Date: Wed, 24 Jun 2026 17:21:35 +0200 Subject: [PATCH 23/27] Update CHANGELOG.md Co-authored-by: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 067b832ca..07bff55be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Added the experimental `options.Experimental.EnableNativeAppHangTracking` (default `false`) to enable app hang detection via `sentry-native` on macOS, Windows, and Linux. On macOS, this requires the crash reporting backend to be switched to `Native` in the Advanced -> Experimental settings. `sentry-native` monitors the main thread and produces an event including a stack trace for the hang, reusing the top-level `AppHangTimeout` (default `5s`). When effective, the Unity SDK's C# watchdog is skipped to avoid duplicate reports. iOS app hang detection remains controlled by the top-level `EnableAppHangTracking`. ([#2709](https://github.com/getsentry/sentry-unity/pull/2709)) +- Added the experimental `options.Experimental.EnableNativeAppHangTracking` (default `false`) to enable app hang detection via `sentry-native` on macOS, Windows, and Linux. On macOS, this requires the macOS backend to be switched to `Native` instead of `Cocoa` in the Advanced -> Experimental settings. `sentry-native` monitors the main thread and produces an event including a stack trace for the hang, reusing the top-level `AppHangTimeout` (default `5s`). When effective, the Unity SDK's C# watchdog is skipped to avoid duplicate reports. iOS app hang detection remains controlled by the top-level `EnableAppHangTracking`. ([#2709](https://github.com/getsentry/sentry-unity/pull/2709)) ## 4.5.0 From 5b384a7df81fb31317b65f6fb91a209bef2d5d76 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 24 Jun 2026 17:27:54 +0200 Subject: [PATCH 24/27] make the heartbeat reentrant --- src/Sentry.Unity.Native/SentryNative.cs | 9 ++++++++- src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Sentry.Unity.Native/SentryNative.cs b/src/Sentry.Unity.Native/SentryNative.cs index 3ea623010..52e604e82 100644 --- a/src/Sentry.Unity.Native/SentryNative.cs +++ b/src/Sentry.Unity.Native/SentryNative.cs @@ -15,6 +15,7 @@ public static class SentryNative private static bool ShouldReinstallBackend; private static IDiagnosticLogger? Logger; + private static Action? OnQuitting; /// /// Configures the native SDK. @@ -56,11 +57,17 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf return; } - ApplicationAdapter.Instance.Quitting += () => + if (OnQuitting is not null) + { + ApplicationAdapter.Instance.Quitting -= OnQuitting; + } + + OnQuitting = () => { Logger?.LogDebug("Closing the sentry-native SDK"); SentryNativeBridge.Close(); }; + ApplicationAdapter.Instance.Quitting += OnQuitting; options.ScopeObserver = new NativeScopeObserver(options); options.EnableScopeSync = true; options.NativeContextWriter = new NativeContextWriter(); diff --git a/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs b/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs index 9ee518233..80d984f4d 100644 --- a/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs +++ b/src/Sentry.Unity/SentryMonoBehaviour.AppHang.cs @@ -13,6 +13,8 @@ public partial class SentryMonoBehaviour { private static readonly TimeSpan AppHangHeartbeatInterval = TimeSpan.FromSeconds(1); + private Coroutine? _appHangHeartbeat; + /// /// Starts the app-hang heartbeat on the main thread at a fixed 1-second interval. Arming is /// deferred until the player loop is running (see ) so @@ -22,8 +24,16 @@ public Coroutine StartAppHangHeartbeat(Action heartbeat) => StartAppHangHeartbeat(heartbeat, AppHangHeartbeatInterval); // Internal overload so tests can use a short interval. - internal Coroutine StartAppHangHeartbeat(Action heartbeat, TimeSpan interval) => - StartCoroutine(AppHangHeartbeatCoroutine(heartbeat, interval)); + internal Coroutine StartAppHangHeartbeat(Action heartbeat, TimeSpan interval) + { + if (_appHangHeartbeat is not null) + { + StopCoroutine(_appHangHeartbeat); + } + + _appHangHeartbeat = StartCoroutine(AppHangHeartbeatCoroutine(heartbeat, interval)); + return _appHangHeartbeat; + } private IEnumerator AppHangHeartbeatCoroutine(Action heartbeat, TimeSpan interval) { From b6665e1ff30e74ebaf388003d5b5e6bb2f4b057d Mon Sep 17 00:00:00 2001 From: Stefan Jandl Date: Wed, 24 Jun 2026 17:29:26 +0200 Subject: [PATCH 25/27] Apply suggestion from @JoshuaMoelans Co-authored-by: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07bff55be..fea0114ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,10 +32,6 @@ - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#351) - [diff](https://github.com/getsentry/sentry-cli/compare/3.5.0...3.5.1) -## Unreleased - -- Extended `EnableAppHangTracking` (default `true`) and `AppHangTimeout` (default `5s`) to enable app hang detection via `sentry-native` on macOS, Windows, and Linux. For macOS this requires the backend to be switched to `native` in the Advanced -> Experimental settings. `sentry-native` monitors the main thread and produces a stack trace for the hang event. When enabled, the Unity SDK's C# watchdog is skipped to avoid duplicate reports. ([#2679](https://github.com/getsentry/sentry-unity/pull/2679)) - ## 4.4.0 ### Features From c5dfc5932ceacc2aa2a7b87dc3361bad4e28ee77 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 24 Jun 2026 17:43:40 +0200 Subject: [PATCH 26/27] added switch stubs --- .../Plugins/Switch/sentry_native_stubs.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/package-dev/Plugins/Switch/sentry_native_stubs.c b/package-dev/Plugins/Switch/sentry_native_stubs.c index a8604748e..15586a5b3 100644 --- a/package-dev/Plugins/Switch/sentry_native_stubs.c +++ b/package-dev/Plugins/Switch/sentry_native_stubs.c @@ -126,6 +126,18 @@ void sentry_options_set_shutdown_timeout(void* options, uint64_t shutdown_timeou (void)shutdown_timeout; } +void sentry_options_set_enable_app_hang_tracking(void* options, int enabled) +{ + (void)options; + (void)enabled; +} + +void sentry_options_set_app_hang_timeout(void* options, uint64_t timeout) +{ + (void)options; + (void)timeout; +} + void sentry_options_set_logger(void* options, void* logger, void* userdata) { (void)options; @@ -315,6 +327,11 @@ void sentry_reinstall_backend(void) /* No-op */ } +void sentry_app_hang_heartbeat(void) +{ + /* No-op */ +} + sentry_value_t sentry_get_modules_list(void) { /* Return null - no modules to report */ From 01be258514052483d7fc83d5001f41bb4bce8290 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 24 Jun 2026 17:50:33 +0200 Subject: [PATCH 27/27] updated changelog --- CHANGELOG.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c026186eb..de9c5ad1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,8 @@ ### Features -- Added the experimental `options.Experimental.EnableNativeAppHangTracking` (default `false`) to enable app hang detection via `sentry-native` on macOS, Windows, and Linux. On macOS, this requires the macOS backend to be switched to `Native` instead of `Cocoa` in the Advanced -> Experimental settings. `sentry-native` monitors the main thread and produces an event including a stack trace for the hang, reusing the top-level `AppHangTimeout` (default `5s`). When effective, the Unity SDK's C# watchdog is skipped to avoid duplicate reports. iOS app hang detection remains controlled by the top-level `EnableAppHangTracking`. ([#2709](https://github.com/getsentry/sentry-unity/pull/2709)) - -## Unreleased - -### Features - - Updated the dependency resolution within the SDK. This enables .NET Standard 2.1 both for building the SDK from source and for setting the Api Compatibility Level in the Player Settings in your project. The assembly aliasing was originally intended to prevent dependency conflicts the SDK might introduce by deeply renaming its bundled dependencies. However, this prevented Unity from resolving these dependencies at build time, creating ambiguity for certain types. To resolve this, the SDK now excludes `System.Buffers`, `System.Memory`, `System.Numerics.Vectors`, and `System.Threading.Tasks.Extensions` from being aliased, letting the Unity build pipeline handle them. ([#2726](https://github.com/getsentry/sentry-unity/pull/2726)) +- Added the experimental `options.Experimental.EnableNativeAppHangTracking` (default `false`) to enable app hang detection via `sentry-native` on macOS, Windows, and Linux. On macOS, this requires the macOS backend to be switched to `Native` instead of `Cocoa` in the Advanced -> Experimental settings. `sentry-native` monitors the main thread and produces an event including a stack trace for the hang, reusing the top-level `AppHangTimeout` (default `5s`). When effective, the Unity SDK's C# watchdog is skipped to avoid duplicate reports. iOS app hang detection remains controlled by the top-level `EnableAppHangTracking`. ([#2709](https://github.com/getsentry/sentry-unity/pull/2709)) ### Dependencies