diff --git a/CHANGELOG.md b/CHANGELOG.md index ba2004921a2..161e7853a33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ### Features - Add `trace_metric_byte` data category and record byte-level client reports when trace metrics are discarded ([#5626](https://github.com/getsentry/sentry-java/pull/5626)) +- Expose sentry-native's heartbeat-based app-hang detection through `SentryAndroidOptions` ([#5623](https://github.com/getsentry/sentry-java/pull/5623)) + - Enable via `setEnableNdkAppHangTracking(true)` (disabled by default) and tune the timeout with `setNdkAppHangTimeoutIntervalMillis(...)` (default `5000` ms), or the `io.sentry.ndk.app-hang.enable` / `io.sentry.ndk.app-hang.timeout-interval-millis` manifest entries + - Intended for hybrid SDKs: emit the heartbeat by calling the native `sentry_app_hang_heartbeat()` from the thread you want monitored. Independent of the JVM-based ANR detection (`setAnrEnabled`) ### Fixes diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 58325d08b5b..0e383125156 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -367,6 +367,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun getDebugImagesLoader ()Lio/sentry/android/core/IDebugImagesLoader; public fun getFrameMetricsCollector ()Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector; public fun getNativeSdkName ()Ljava/lang/String; + public fun getNdkAppHangTimeoutIntervalMillis ()J public fun getNdkHandlerStrategy ()I public fun getScreenshot ()Lio/sentry/android/core/SentryScreenshotOptions; public fun getStartupCrashDurationThresholdMillis ()J @@ -388,6 +389,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun isEnableAutoTraceIdGeneration ()Z public fun isEnableFramesTracking ()Z public fun isEnableNdk ()Z + public fun isEnableNdkAppHangTracking ()Z public fun isEnableNetworkEventBreadcrumbs ()Z public fun isEnablePerformanceV2 ()Z public fun isEnableRootCheck ()Z @@ -420,6 +422,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setEnableAutoTraceIdGeneration (Z)V public fun setEnableFramesTracking (Z)V public fun setEnableNdk (Z)V + public fun setEnableNdkAppHangTracking (Z)V public fun setEnableNetworkEventBreadcrumbs (Z)V public fun setEnablePerformanceV2 (Z)V public fun setEnableRootCheck (Z)V @@ -430,6 +433,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setFrameMetricsCollector (Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V public fun setNativeHandlerStrategy (Lio/sentry/android/core/NdkHandlerStrategy;)V public fun setNativeSdkName (Ljava/lang/String;)V + public fun setNdkAppHangTimeoutIntervalMillis (J)V public fun setReportHistoricalAnrs (Z)V public fun setReportHistoricalTombstones (Z)V public fun setTombstoneEnabled (Z)V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index c34ee0dbfa9..6700ad81599 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -35,6 +35,11 @@ final class ManifestMetadataReader { static final String ANR_ATTACH_THREAD_DUMPS = "io.sentry.anr.attach-thread-dumps"; static final String ANR_REPORT_HISTORICAL = "io.sentry.anr.report-historical"; + static final String NDK_APP_HANG_TRACKING_ENABLE = "io.sentry.ndk.app-hang.enable"; + + static final String NDK_APP_HANG_TIMEOUT_INTERVAL_MILLIS = + "io.sentry.ndk.app-hang.timeout-interval-millis"; + static final String TOMBSTONE_ENABLE = "io.sentry.tombstone.enable"; static final String TOMBSTONE_ATTACH_RAW = "io.sentry.tombstone.attach-raw"; @@ -264,6 +269,20 @@ static void applyMetadata( options.setReportHistoricalAnrs( readBool(metadata, logger, ANR_REPORT_HISTORICAL, options.isReportHistoricalAnrs())); + options.setEnableNdkAppHangTracking( + readBool( + metadata, + logger, + NDK_APP_HANG_TRACKING_ENABLE, + options.isEnableNdkAppHangTracking())); + + options.setNdkAppHangTimeoutIntervalMillis( + readLong( + metadata, + logger, + NDK_APP_HANG_TIMEOUT_INTERVAL_MILLIS, + options.getNdkAppHangTimeoutIntervalMillis())); + final @Nullable String dsn = readString(metadata, logger, DSN, options.getDsn()); final boolean enabled = readBool(metadata, logger, ENABLE_SENTRY, options.isEnabled()); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index ed07c4edaaf..615db97a28d 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -37,6 +37,22 @@ public final class SentryAndroidOptions extends SentryOptions { /** Enable or disable ANR on Debug mode Default is disabled Used by AnrIntegration */ private boolean anrReportInDebug = false; + /** + * Enable or disable in-process, heartbeat-based app-hang detection in sentry-native. Default is + * disabled. When enabled, sentry-native's background watchdog captures an app-hang event if no + * heartbeat is received within {@link #ndkAppHangTimeoutIntervalMillis} on the monitored thread. + * + *
This is intended for downstream/hybrid SDKs that emit the heartbeat by calling the native
+ * {@code sentry_app_hang_heartbeat()} from their main thread. It is independent of the JVM-based
+ * {@link #anrEnabled} ANR detection.
+ */
+ private boolean enableNdkAppHangTracking = false;
+
+ /**
+ * The app-hang detection timeout interval in millis used by sentry-native. Default is 5000 = 5s.
+ */
+ private long ndkAppHangTimeoutIntervalMillis = 5000;
+
/**
* Enable or disable automatic breadcrumbs for Activity lifecycle. Using
* Application.ActivityLifecycleCallbacks
@@ -338,6 +354,50 @@ public void setAnrReportInDebug(boolean anrReportInDebug) {
this.anrReportInDebug = anrReportInDebug;
}
+ /**
+ * Checks if heartbeat-based app-hang detection in sentry-native is enabled. Default is disabled.
+ *
+ * @return true if enabled or false otherwise
+ */
+ @ApiStatus.Experimental
+ public boolean isEnableNdkAppHangTracking() {
+ return enableNdkAppHangTracking;
+ }
+
+ /**
+ * Enables or disables heartbeat-based app-hang detection in sentry-native. Default is disabled.
+ * Requires the NDK integration to be present and emitting heartbeats via the native {@code
+ * sentry_app_hang_heartbeat()}.
+ *
+ * @param enableNdkAppHangTracking true for enabled and false for disabled
+ */
+ @ApiStatus.Experimental
+ public void setEnableNdkAppHangTracking(boolean enableNdkAppHangTracking) {
+ this.enableNdkAppHangTracking = enableNdkAppHangTracking;
+ }
+
+ /**
+ * Returns the app-hang detection timeout interval in millis used by sentry-native. Default is
+ * 5000 = 5s.
+ *
+ * @return the timeout in millis
+ */
+ @ApiStatus.Experimental
+ public long getNdkAppHangTimeoutIntervalMillis() {
+ return ndkAppHangTimeoutIntervalMillis;
+ }
+
+ /**
+ * Sets the app-hang detection timeout interval in millis used by sentry-native. Default is 5000 =
+ * 5s.
+ *
+ * @param ndkAppHangTimeoutIntervalMillis the timeout interval in millis
+ */
+ @ApiStatus.Experimental
+ public void setNdkAppHangTimeoutIntervalMillis(long ndkAppHangTimeoutIntervalMillis) {
+ this.ndkAppHangTimeoutIntervalMillis = ndkAppHangTimeoutIntervalMillis;
+ }
+
/**
* Sets Tombstone reporting (ApplicationExitInfo.REASON_CRASH_NATIVE) to enabled or disabled.
*
diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt
index 3b12e6489a7..7814362b588 100644
--- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt
+++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt
@@ -288,6 +288,56 @@ class ManifestMetadataReaderTest {
assertEquals(false, fixture.options.isAttachAnrThreadDump)
}
+ @Test
+ fun `applyMetadata reads app hang tracking enabled to options`() {
+ // Arrange
+ val bundle = bundleOf(ManifestMetadataReader.NDK_APP_HANG_TRACKING_ENABLE to true)
+ val context = fixture.getContext(metaData = bundle)
+
+ // Act
+ ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
+
+ // Assert
+ assertEquals(true, fixture.options.isEnableNdkAppHangTracking)
+ }
+
+ @Test
+ fun `applyMetadata reads app hang tracking enabled to options and keeps default`() {
+ // Arrange
+ val context = fixture.getContext()
+
+ // Act
+ ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
+
+ // Assert
+ assertEquals(false, fixture.options.isEnableNdkAppHangTracking)
+ }
+
+ @Test
+ fun `applyMetadata reads app hang timeout interval to options`() {
+ // Arrange
+ val bundle = bundleOf(ManifestMetadataReader.NDK_APP_HANG_TIMEOUT_INTERVAL_MILLIS to 1000)
+ val context = fixture.getContext(metaData = bundle)
+
+ // Act
+ ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
+
+ // Assert
+ assertEquals(1000.toLong(), fixture.options.ndkAppHangTimeoutIntervalMillis)
+ }
+
+ @Test
+ fun `applyMetadata reads app hang timeout interval to options and keeps default`() {
+ // Arrange
+ val context = fixture.getContext()
+
+ // Act
+ ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
+
+ // Assert
+ assertEquals(5000.toLong(), fixture.options.ndkAppHangTimeoutIntervalMillis)
+ }
+
@Test
fun `applyMetadata reads tombstone attach raw to options`() {
// Arrange
diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt
index 0eec9502702..94857b91058 100644
--- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt
+++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt
@@ -239,6 +239,13 @@ class SentryAndroidOptionsTest {
sentryOptions.anrProfilingSampleRate = 2.0
}
+ @Test
+ fun `app hang tracking is disabled by default with a 5s timeout`() {
+ val sentryOptions = SentryAndroidOptions()
+ assertFalse(sentryOptions.isEnableNdkAppHangTracking)
+ assertEquals(5000L, sentryOptions.ndkAppHangTimeoutIntervalMillis)
+ }
+
private class CustomDebugImagesLoader : IDebugImagesLoader {
override fun loadDebugImages(): List