diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b7e6ba02..58c819bed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ ### 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)) +- Added `EnableAppHangTracking` (default `true`) and `AppHangTimeout` (default `5s`) to enable app hang detection via the native SDK. Currently effective on iOS through `sentry-cocoa`, which monitors the main thread and produces a stack trace for the hang event. On other platforms this is a no-op until each platform's native hang detection lands. When enabled on iOS, the Unity SDK's C# watchdog is skipped to avoid duplicate reports ([#2679](https://github.com/getsentry/sentry-unity/pull/2679)) +- Added `AndroidNativeAnrEnabled` (default `true`) to enable ANR detection through the `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 diff --git a/package-dev/Plugins/iOS/SentryNativeBridge.m b/package-dev/Plugins/iOS/SentryNativeBridge.m index afb91ae36..8adde3b88 100644 --- a/package-dev/Plugins/iOS/SentryNativeBridge.m +++ b/package-dev/Plugins/iOS/SentryNativeBridge.m @@ -66,6 +66,12 @@ void SentryNativeBridgeOptionsSetInt(const void *options, const char *name, int3 dictOptions[[NSString stringWithUTF8String:name]] = [NSNumber numberWithInt:value]; } +void SentryNativeBridgeOptionsSetDouble(const void *options, const char *name, double value) +{ + NSMutableDictionary *dictOptions = (__bridge NSMutableDictionary *)options; + dictOptions[[NSString stringWithUTF8String:name]] = [NSNumber numberWithDouble:value]; +} + void SentryNativeBridgeOptionsAddFailedRequestStatusCodeRange(const void *options, int32_t min, int32_t max) { NSMutableDictionary *dictOptions = (__bridge NSMutableDictionary *)options; diff --git a/package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m b/package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m index f504c452d..15a30557f 100644 --- a/package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m +++ b/package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m @@ -6,6 +6,7 @@ void *_Nullable SentryNativeBridgeOptionsNew() { return nil; } void SentryNativeBridgeOptionsSetString(void *options, const char *name, const char *value) { } void SentryNativeBridgeOptionsSetInt(void *options, const char *name, int32_t value) { } +void SentryNativeBridgeOptionsSetDouble(void *options, const char *name, double value) { } void SentryNativeBridgeOptionsAddFailedRequestStatusCodeRange(void *options, int32_t min, int32_t max) { } int SentryNativeBridgeStartWithOptions(void *options) { return 0; } diff --git a/package-dev/Plugins/macOS/SentryNativeBridge.m b/package-dev/Plugins/macOS/SentryNativeBridge.m index 7fba6678c..4e6efb71e 100644 --- a/package-dev/Plugins/macOS/SentryNativeBridge.m +++ b/package-dev/Plugins/macOS/SentryNativeBridge.m @@ -121,6 +121,12 @@ void SentryNativeBridgeOptionsSetInt(const void *options, const char *name, int3 dictOptions[[NSString stringWithUTF8String:name]] = [NSNumber numberWithInt:value]; } +void SentryNativeBridgeOptionsSetDouble(const void *options, const char *name, double value) +{ + NSMutableDictionary *dictOptions = (__bridge NSMutableDictionary *)options; + dictOptions[[NSString stringWithUTF8String:name]] = [NSNumber numberWithDouble:value]; +} + void SentryNativeBridgeOptionsAddFailedRequestStatusCodeRange(const void *options, int32_t min, int32_t max) { NSMutableDictionary *dictOptions = (__bridge NSMutableDictionary *)options; diff --git a/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset b/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset index 94646d096..e3f7ddd72 100644 --- a/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset +++ b/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset @@ -11,7 +11,7 @@ MonoBehaviour: m_EditorHideFlags: 0 m_Script: {fileID: -668357930, guid: 43ec428a58422470fa764bdba9d9bc19, type: 3} m_Name: SentryOptions - m_EditorClassIdentifier: + m_EditorClassIdentifier: k__BackingField: 1 k__BackingField: https://e9ee299dbf554dfd930bc5f3c90d5d4b@o447951.ingest.us.sentry.io/4504604988538880 k__BackingField: 1 @@ -27,8 +27,8 @@ MonoBehaviour: k__BackingField: 0 k__BackingField: 1 k__BackingField: 30000 - k__BackingField: - k__BackingField: + k__BackingField: + k__BackingField: k__BackingField: 1 k__BackingField: 1 k__BackingField: 1 @@ -61,6 +61,8 @@ MonoBehaviour: k__BackingField: 30 k__BackingField: 0 k__BackingField: 5000 + k__BackingField: 1 + k__BackingField: 5000 k__BackingField: 1 k__BackingField: f401000057020000 k__BackingField: 1 diff --git a/src/Sentry.Unity.Editor.iOS/NativeOptions.cs b/src/Sentry.Unity.Editor.iOS/NativeOptions.cs index 6a0752d60..6de8450e4 100644 --- a/src/Sentry.Unity.Editor.iOS/NativeOptions.cs +++ b/src/Sentry.Unity.Editor.iOS/NativeOptions.cs @@ -30,7 +30,8 @@ internal static string Generate(SentryUnityOptions options) @""maxBreadcrumbs"": @{options.MaxBreadcrumbs}, @""maxCacheItems"": @{options.MaxCacheItems}, @""enableAutoSessionTracking"": @NO, - @""enableAppHangTracking"": @NO, + @""enableAppHangTracking"": @{ToObjCString(options.EnableAppHangTracking)}, + @""appHangTimeoutInterval"": @{options.AppHangTimeout.TotalSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture)}, @""enableCaptureFailedRequests"": @{ToObjCString(options.CaptureFailedRequests)}, @""failedRequestStatusCodes"" : @[{failedRequestStatusCodesArray}], @""sendDefaultPii"" : @{ToObjCString(options.SendDefaultPii)}, diff --git a/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs b/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs index 56d0c2e9e..8b255781b 100644 --- a/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs +++ b/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs @@ -39,21 +39,40 @@ internal static void Display(ScriptableSentryUnityOptions options, SentryCliOpti EditorGUILayout.Space(); { - options.AnrDetectionEnabled = EditorGUILayout.BeginToggleGroup( - new GUIContent("ANR Detection", "Whether the SDK should report 'Application Not " + - "Responding' events."), + GUILayout.Label("C# Watchdog", EditorStyles.boldLabel); + + options.AnrDetectionEnabled = EditorGUILayout.Toggle( + new GUIContent("Enable", "Whether the SDK should run the C# main-thread watchdog " + + "to report 'Application Not Responding' events."), options.AnrDetectionEnabled); - EditorGUI.indentLevel++; options.AnrTimeout = EditorGUILayout.IntField( - new GUIContent("Timeout [ms]", + new GUIContent("Watchdog Timeout [ms]", "The duration in [ms] for how long the game has to be unresponsive " + - "before an ANR event is reported.\nDefault: 5000ms"), + "before the C# watchdog reports an ANR event.\nDefault: 5000ms"), options.AnrTimeout); options.AnrTimeout = Math.Max(0, options.AnrTimeout); + } - EditorGUI.indentLevel--; - EditorGUILayout.EndToggleGroup(); + EditorGUILayout.Space(); + EditorGUI.DrawRect(EditorGUILayout.GetControlRect(false, 1), Color.gray); + EditorGUILayout.Space(); + + { + GUILayout.Label("App Hang Tracking", EditorStyles.boldLabel); + + options.EnableAppHangTracking = EditorGUILayout.Toggle( + new GUIContent("Enable", + "Enables app hang detection via the native SDK. Currently effective on iOS only; " + + "no-op on other platforms until each platform's native hang detection lands."), + 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"), + options.AppHangTimeout); + options.AppHangTimeout = Math.Max(0, options.AppHangTimeout); } EditorGUILayout.Space(); diff --git a/src/Sentry.Unity.Editor/ScriptableSentryUnityOptionsEditor.cs b/src/Sentry.Unity.Editor/ScriptableSentryUnityOptionsEditor.cs index 7f7169853..f1dc8f3b2 100644 --- a/src/Sentry.Unity.Editor/ScriptableSentryUnityOptionsEditor.cs +++ b/src/Sentry.Unity.Editor/ScriptableSentryUnityOptionsEditor.cs @@ -68,9 +68,13 @@ public override void OnInspectorGUI() EditorGUI.DrawRect(EditorGUILayout.GetControlRect(false, 1), Color.gray); EditorGUILayout.Space(); - EditorGUILayout.LabelField("Application Not Responding", EditorStyles.boldLabel); - EditorGUILayout.Toggle("Enable ANR Detection", options.AnrDetectionEnabled); - EditorGUILayout.IntField("ANR Timeout [ms]", options.AnrTimeout); + EditorGUILayout.LabelField("C# Watchdog", EditorStyles.boldLabel); + EditorGUILayout.Toggle("Enable", options.AnrDetectionEnabled); + EditorGUILayout.IntField("Watchdog Timeout [ms]", options.AnrTimeout); + + EditorGUILayout.LabelField("App Hang Tracking", EditorStyles.boldLabel); + EditorGUILayout.Toggle("Enable", options.EnableAppHangTracking); + EditorGUILayout.IntField("App Hang Timeout [ms]", options.AppHangTimeout); EditorGUILayout.Space(); EditorGUI.DrawRect(EditorGUILayout.GetControlRect(false, 1), Color.gray); diff --git a/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs b/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs index 9d36d8d6a..88f88c8fb 100644 --- a/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs +++ b/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs @@ -66,6 +66,12 @@ public static bool Init(SentryUnityOptions options) // See https://github.com/getsentry/sentry-unity/issues/1658 OptionsSetInt(cOptions, "enableNetworkBreadcrumbs", 0); + Logger?.LogDebug("Setting EnableAppHangTracking: {0}", options.EnableAppHangTracking); + OptionsSetInt(cOptions, "enableAppHangTracking", options.EnableAppHangTracking ? 1 : 0); + + Logger?.LogDebug("Setting AppHangTimeoutInterval: {0}s", options.AppHangTimeout.TotalSeconds); + OptionsSetDouble(cOptions, "appHangTimeoutInterval", options.AppHangTimeout.TotalSeconds); + Logger?.LogDebug("Setting EnableWatchdogTerminationTracking: {0}", options.IosWatchdogTerminationIntegrationEnabled); OptionsSetInt(cOptions, "enableWatchdogTerminationTracking", options.IosWatchdogTerminationIntegrationEnabled ? 1 : 0); @@ -103,6 +109,9 @@ public static bool Init(SentryUnityOptions options) [DllImport("__Internal", EntryPoint = "SentryNativeBridgeOptionsSetInt")] private static extern void OptionsSetInt(IntPtr options, string name, int value); + [DllImport("__Internal", EntryPoint = "SentryNativeBridgeOptionsSetDouble")] + private static extern void OptionsSetDouble(IntPtr options, string name, double value); + [DllImport("__Internal", EntryPoint = "SentryNativeBridgeOptionsAddFailedRequestStatusCodeRange")] private static extern void OptionsAddFailedRequestStatusCodeRange(IntPtr options, int min, int max); diff --git a/src/Sentry.Unity.iOS/SentryNativeCocoa.cs b/src/Sentry.Unity.iOS/SentryNativeCocoa.cs index 89aa52f04..238b9a5fb 100644 --- a/src/Sentry.Unity.iOS/SentryNativeCocoa.cs +++ b/src/Sentry.Unity.iOS/SentryNativeCocoa.cs @@ -43,6 +43,13 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf } options.ScopeObserver = new NativeScopeObserver("iOS", options); + + if (options.EnableAppHangTracking) + { + Logger?.LogDebug("Disabling the C# ANR watchdog - sentry-cocoa handles app hang detection."); + options.DisableAnrIntegration(); + } + } else { diff --git a/src/Sentry.Unity/ScriptableSentryUnityOptions.cs b/src/Sentry.Unity/ScriptableSentryUnityOptions.cs index e357b6724..4101f786f 100644 --- a/src/Sentry.Unity/ScriptableSentryUnityOptions.cs +++ b/src/Sentry.Unity/ScriptableSentryUnityOptions.cs @@ -108,6 +108,8 @@ public static string GetConfigPath(string? notDefaultConfigName = null) [field: SerializeField] public bool AnrDetectionEnabled { get; set; } = true; [field: SerializeField] public int AnrTimeout { get; set; } = (int)TimeSpan.FromSeconds(5).TotalMilliseconds; + [field: SerializeField] public bool EnableAppHangTracking { get; set; } = true; + [field: SerializeField] public int AppHangTimeout { get; set; } = (int)TimeSpan.FromSeconds(5).TotalMilliseconds; [field: SerializeField] public bool CaptureFailedRequests { get; set; } = true; @@ -201,6 +203,8 @@ internal SentryUnityOptions ToSentryUnityOptions( DiagnosticLevel = DiagnosticLevel, CaptureLogErrorEvents = CaptureLogErrorEvents, AnrTimeout = TimeSpan.FromMilliseconds(AnrTimeout), + EnableAppHangTracking = EnableAppHangTracking, + AppHangTimeout = TimeSpan.FromMilliseconds(AppHangTimeout), CaptureFailedRequests = CaptureFailedRequests, FilterBadGatewayExceptions = FilterBadGatewayExceptions, IosNativeSupportEnabled = IosNativeSupportEnabled, diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 20da43cb0..74f8babfd 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -185,6 +185,19 @@ 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. + /// + public bool EnableAppHangTracking { get; set; } = true; + + /// + /// The minimum duration for which the main thread must be blocked before + /// reports an app hang. + /// + public TimeSpan AppHangTimeout { get; set; } = TimeSpan.FromSeconds(5); + /// /// Whether the SDK should initialize the native SDK before the game starts. This bakes the options at build-time into /// the generated Xcode project. Modifying the options at runtime will not affect the options used to initialize diff --git a/test/Sentry.Unity.Editor.iOS.Tests/NativeOptionsTests.cs b/test/Sentry.Unity.Editor.iOS.Tests/NativeOptionsTests.cs index 71aecac29..acb654359 100644 --- a/test/Sentry.Unity.Editor.iOS.Tests/NativeOptionsTests.cs +++ b/test/Sentry.Unity.Editor.iOS.Tests/NativeOptionsTests.cs @@ -58,6 +58,46 @@ public void CreateOptionsFile_NewSentryOptions_ContainsSdkNameSetting() File.Delete(testOptionsFileName); } + [Test] + public void CreateOptionsFile_EnableAppHangTracking_SetsYes() + { + const string testOptionsFileName = "testOptions.m"; + + NativeOptions.CreateFile(testOptionsFileName, new SentryUnityOptions { EnableAppHangTracking = true }); + + var nativeOptions = File.ReadAllText(testOptionsFileName); + StringAssert.Contains("@\"enableAppHangTracking\": @YES", nativeOptions); + + File.Delete(testOptionsFileName); + } + + [Test] + public void CreateOptionsFile_AppHangTrackingDisabled_SetsNo() + { + const string testOptionsFileName = "testOptions.m"; + + NativeOptions.CreateFile(testOptionsFileName, new SentryUnityOptions { EnableAppHangTracking = false }); + + var nativeOptions = File.ReadAllText(testOptionsFileName); + StringAssert.Contains("@\"enableAppHangTracking\": @NO", nativeOptions); + + File.Delete(testOptionsFileName); + } + + [Test] + public void CreateOptionsFile_AppHangTimeout_WrittenAsSeconds() + { + const string testOptionsFileName = "testOptions.m"; + + NativeOptions.CreateFile(testOptionsFileName, + new SentryUnityOptions { AppHangTimeout = System.TimeSpan.FromMilliseconds(7500) }); + + var nativeOptions = File.ReadAllText(testOptionsFileName); + StringAssert.Contains("@\"appHangTimeoutInterval\": @7.5", nativeOptions); + + File.Delete(testOptionsFileName); + } + [Test] public void CreateOptionsFile_FilterBadGatewayEnabled_AddsFiltering() {