From 4c902a816e003e7dbe83e664069d9a04ccbd6c1b Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 24 Jun 2026 17:07:27 +0200 Subject: [PATCH 1/7] forwarding app hang options --- .../api/sentry-android-core.api | 4 ++ .../android/core/ManifestMetadataReader.java | 16 +++++ .../android/core/SentryAndroidOptions.java | 60 +++++++++++++++++++ .../core/ManifestMetadataReaderTest.kt | 50 ++++++++++++++++ .../android/core/SentryAndroidOptionsTest.kt | 7 +++ .../java/io/sentry/android/ndk/SentryNdk.java | 3 + .../io/sentry/android/ndk/SentryNdkTest.kt | 28 +++++++++ 7 files changed, 168 insertions(+) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 58325d08b5b..511fb2f301c 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -362,6 +362,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun enableAllAutoBreadcrumbs (Z)V public fun getAnrProfilingSampleRate ()Ljava/lang/Double; public fun getAnrTimeoutIntervalMillis ()J + public fun getAppHangTimeoutIntervalMillis ()J public fun getBeforeScreenshotCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback; public fun getBeforeViewHierarchyCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback; public fun getDebugImagesLoader ()Lio/sentry/android/core/IDebugImagesLoader; @@ -383,6 +384,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun isEnableActivityLifecycleTracingAutoFinish ()Z public fun isEnableAnrFingerprinting ()Z public fun isEnableAppComponentBreadcrumbs ()Z + public fun isEnableAppHangTracking ()Z public fun isEnableAppLifecycleBreadcrumbs ()Z public fun isEnableAutoActivityLifecycleTracing ()Z public fun isEnableAutoTraceIdGeneration ()Z @@ -402,6 +404,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setAnrProfilingSampleRate (Ljava/lang/Double;)V public fun setAnrReportInDebug (Z)V public fun setAnrTimeoutIntervalMillis (J)V + public fun setAppHangTimeoutIntervalMillis (J)V public fun setAttachAnrThreadDump (Z)V public fun setAttachRawTombstone (Z)V public fun setAttachScreenshot (Z)V @@ -415,6 +418,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setEnableActivityLifecycleTracingAutoFinish (Z)V public fun setEnableAnrFingerprinting (Z)V public fun setEnableAppComponentBreadcrumbs (Z)V + public fun setEnableAppHangTracking (Z)V public fun setEnableAppLifecycleBreadcrumbs (Z)V public fun setEnableAutoActivityLifecycleTracing (Z)V public fun setEnableAutoTraceIdGeneration (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..8ad0f46cf38 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 APP_HANG_TRACKING_ENABLE = "io.sentry.app-hang.enable"; + + static final String APP_HANG_TIMEOUT_INTERVAL_MILLIS = + "io.sentry.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,17 @@ static void applyMetadata( options.setReportHistoricalAnrs( readBool(metadata, logger, ANR_REPORT_HISTORICAL, options.isReportHistoricalAnrs())); + options.setEnableAppHangTracking( + readBool( + metadata, logger, APP_HANG_TRACKING_ENABLE, options.isEnableAppHangTracking())); + + options.setAppHangTimeoutIntervalMillis( + readLong( + metadata, + logger, + APP_HANG_TIMEOUT_INTERVAL_MILLIS, + options.getAppHangTimeoutIntervalMillis())); + 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..ac31b5bcf32 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 #appHangTimeoutIntervalMillis} 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 enableAppHangTracking = false; + + /** + * The app-hang detection timeout interval in millis used by sentry-native. Default is 5000 = 5s. + */ + private long appHangTimeoutIntervalMillis = 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 isEnableAppHangTracking() { + return enableAppHangTracking; + } + + /** + * 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 enableAppHangTracking true for enabled and false for disabled + */ + @ApiStatus.Experimental + public void setEnableAppHangTracking(boolean enableAppHangTracking) { + this.enableAppHangTracking = enableAppHangTracking; + } + + /** + * 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 getAppHangTimeoutIntervalMillis() { + return appHangTimeoutIntervalMillis; + } + + /** + * Sets the app-hang detection timeout interval in millis used by sentry-native. Default is 5000 = + * 5s. + * + * @param appHangTimeoutIntervalMillis the timeout interval in millis + */ + @ApiStatus.Experimental + public void setAppHangTimeoutIntervalMillis(long appHangTimeoutIntervalMillis) { + this.appHangTimeoutIntervalMillis = appHangTimeoutIntervalMillis; + } + /** * 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..0345b5885e2 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.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.isEnableAppHangTracking) + } + + @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.isEnableAppHangTracking) + } + + @Test + fun `applyMetadata reads app hang timeout interval to options`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.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.appHangTimeoutIntervalMillis) + } + + @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.appHangTimeoutIntervalMillis) + } + @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..fc08bc9cf91 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.isEnableAppHangTracking) + assertEquals(5000L, sentryOptions.appHangTimeoutIntervalMillis) + } + private class CustomDebugImagesLoader : IDebugImagesLoader { override fun loadDebugImages(): List? = null diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java index 9d6d64a1236..4d37e282da1 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java @@ -73,6 +73,9 @@ public static void init(@NotNull final SentryAndroidOptions options) { ndkOptions.setTracesSampleRate(tracesSampleRate.floatValue()); } + ndkOptions.setEnableAppHangTracking(options.isEnableAppHangTracking()); + ndkOptions.setAppHangTimeoutMillis(options.getAppHangTimeoutIntervalMillis()); + //noinspection UnstableApiUsage io.sentry.ndk.SentryNdk.init(ndkOptions); diff --git a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/SentryNdkTest.kt b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/SentryNdkTest.kt index c9f540f0afa..f5c1ca64518 100644 --- a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/SentryNdkTest.kt +++ b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/SentryNdkTest.kt @@ -3,7 +3,9 @@ package io.sentry.android.ndk import io.sentry.android.core.SentryAndroidOptions import io.sentry.ndk.NdkOptions import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull +import kotlin.test.assertTrue import org.junit.Test import org.mockito.Mockito import org.mockito.kotlin.any @@ -68,4 +70,30 @@ class SentryNdkTest { assertEquals(0.75f, fixture.capturedOptions!!.tracesSampleRate, 0.0001f) } } + + @Test + fun `SentryNdk does not enable app hang tracking by default`() { + fixture.getSut { + assertNotNull(fixture.capturedOptions) + assertFalse(fixture.capturedOptions!!.isEnableAppHangTracking) + assertEquals(5000L, fixture.capturedOptions!!.appHangTimeoutMillis) + } + } + + @Test + fun `SentryNdk propagates app hang tracking options`() { + fixture.getSut( + options = + SentryAndroidOptions().apply { + dsn = "https://key@sentry.io/proj" + cacheDirPath = "/cache" + isEnableAppHangTracking = true + appHangTimeoutIntervalMillis = 2000 + } + ) { + assertNotNull(fixture.capturedOptions) + assertTrue(fixture.capturedOptions!!.isEnableAppHangTracking) + assertEquals(2000L, fixture.capturedOptions!!.appHangTimeoutMillis) + } + } } From 8d77cd7df5b984c0cd76ca3a9a644d2564285903 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 24 Jun 2026 17:11:37 +0200 Subject: [PATCH 2/7] updated changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d66d755a7c..494bb7dbeba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +### Features + +- Expose sentry-native's heartbeat-based app-hang detection through `SentryAndroidOptions` ([#5623](https://github.com/getsentry/sentry-java/pull/5623)) + - Enable via `setEnableAppHangTracking(true)` (disabled by default) and tune the timeout with `setAppHangTimeoutIntervalMillis(...)` (default `5000` ms), or the `io.sentry.app-hang.enable` / `io.sentry.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 - Release `MediaMuxer` when a replay segment has no encodable frames to avoid a resource leak ([#5583](https://github.com/getsentry/sentry-java/pull/5583)) From 770d0555406ff9636e14bb6fd07186e5e6b3e9e3 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 30 Jun 2026 16:50:56 +0200 Subject: [PATCH 3/7] tweak scope application to deduplicate breadcrumbs --- sentry/src/main/java/io/sentry/SentryClient.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index a739eddd9d9..51a750f8c12 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -974,7 +974,7 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint } if (shouldApplyScopeData(transaction, hint)) { - transaction = applyScope(transaction, scope); + transaction = applyScope(transaction, scope, HintUtils.hasType(hint, Cached.class)); if (transaction != null && scope != null) { transaction = processTransaction(transaction, hint, scope.getEventProcessors()); @@ -1381,7 +1381,7 @@ public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metri private @Nullable SentryEvent applyScope( @NotNull SentryEvent event, final @Nullable IScope scope, final @NotNull Hint hint) { if (scope != null) { - applyScope(event, scope); + applyScope(event, scope, HintUtils.hasType(hint, Cached.class)); if (event.getTransaction() == null) { event.setTransaction(scope.getTransactionName()); @@ -1513,7 +1513,7 @@ public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metri } private @NotNull T applyScope( - final @NotNull T sentryBaseEvent, final @Nullable IScope scope) { + final @NotNull T sentryBaseEvent, final @Nullable IScope scope, final boolean isCached) { if (scope != null) { if (sentryBaseEvent.getRequest() == null) { sentryBaseEvent.setRequest(scope.getRequest()); @@ -1532,7 +1532,9 @@ public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metri } if (sentryBaseEvent.getBreadcrumbs() == null) { sentryBaseEvent.setBreadcrumbs(new ArrayList<>(scope.getBreadcrumbs())); - } else { + } else if (!isCached) { + // A Cached event comes from the outbox and already carries its own breadcrumbs (e.g. native + // events written by sentry-native). Appending the scope's breadcrumbs would duplicate them. sortBreadcrumbsByDate(sentryBaseEvent, scope.getBreadcrumbs()); } if (sentryBaseEvent.getExtras() == null) { From c8f7b88520b54ed0cb396c816fef14e641250b07 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 6 Jul 2026 11:31:07 +0200 Subject: [PATCH 4/7] prefix it with ndk --- CHANGELOG.md | 2 +- .../api/sentry-android-core.api | 8 +++--- .../android/core/ManifestMetadataReader.java | 19 ++++++++------ .../android/core/SentryAndroidOptions.java | 26 +++++++++---------- .../core/ManifestMetadataReaderTest.kt | 12 ++++----- .../android/core/SentryAndroidOptionsTest.kt | 4 +-- .../java/io/sentry/android/ndk/SentryNdk.java | 4 +-- .../io/sentry/android/ndk/SentryNdkTest.kt | 4 +-- 8 files changed, 41 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97adeb8e020..161e7853a33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - 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 `setEnableAppHangTracking(true)` (disabled by default) and tune the timeout with `setAppHangTimeoutIntervalMillis(...)` (default `5000` ms), or the `io.sentry.app-hang.enable` / `io.sentry.app-hang.timeout-interval-millis` manifest entries + - 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 511fb2f301c..0e383125156 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -362,12 +362,12 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun enableAllAutoBreadcrumbs (Z)V public fun getAnrProfilingSampleRate ()Ljava/lang/Double; public fun getAnrTimeoutIntervalMillis ()J - public fun getAppHangTimeoutIntervalMillis ()J public fun getBeforeScreenshotCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback; public fun getBeforeViewHierarchyCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback; 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 @@ -384,12 +384,12 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun isEnableActivityLifecycleTracingAutoFinish ()Z public fun isEnableAnrFingerprinting ()Z public fun isEnableAppComponentBreadcrumbs ()Z - public fun isEnableAppHangTracking ()Z public fun isEnableAppLifecycleBreadcrumbs ()Z public fun isEnableAutoActivityLifecycleTracing ()Z 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 @@ -404,7 +404,6 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setAnrProfilingSampleRate (Ljava/lang/Double;)V public fun setAnrReportInDebug (Z)V public fun setAnrTimeoutIntervalMillis (J)V - public fun setAppHangTimeoutIntervalMillis (J)V public fun setAttachAnrThreadDump (Z)V public fun setAttachRawTombstone (Z)V public fun setAttachScreenshot (Z)V @@ -418,12 +417,12 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setEnableActivityLifecycleTracingAutoFinish (Z)V public fun setEnableAnrFingerprinting (Z)V public fun setEnableAppComponentBreadcrumbs (Z)V - public fun setEnableAppHangTracking (Z)V public fun setEnableAppLifecycleBreadcrumbs (Z)V public fun setEnableAutoActivityLifecycleTracing (Z)V 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 @@ -434,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 8ad0f46cf38..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,10 +35,10 @@ 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 APP_HANG_TRACKING_ENABLE = "io.sentry.app-hang.enable"; + static final String NDK_APP_HANG_TRACKING_ENABLE = "io.sentry.ndk.app-hang.enable"; - static final String APP_HANG_TIMEOUT_INTERVAL_MILLIS = - "io.sentry.app-hang.timeout-interval-millis"; + 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"; @@ -269,16 +269,19 @@ static void applyMetadata( options.setReportHistoricalAnrs( readBool(metadata, logger, ANR_REPORT_HISTORICAL, options.isReportHistoricalAnrs())); - options.setEnableAppHangTracking( + options.setEnableNdkAppHangTracking( readBool( - metadata, logger, APP_HANG_TRACKING_ENABLE, options.isEnableAppHangTracking())); + metadata, + logger, + NDK_APP_HANG_TRACKING_ENABLE, + options.isEnableNdkAppHangTracking())); - options.setAppHangTimeoutIntervalMillis( + options.setNdkAppHangTimeoutIntervalMillis( readLong( metadata, logger, - APP_HANG_TIMEOUT_INTERVAL_MILLIS, - options.getAppHangTimeoutIntervalMillis())); + 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 ac31b5bcf32..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 @@ -40,18 +40,18 @@ public final class SentryAndroidOptions extends SentryOptions { /** * 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 #appHangTimeoutIntervalMillis} on the monitored thread. + * 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 enableAppHangTracking = false; + private boolean enableNdkAppHangTracking = false; /** * The app-hang detection timeout interval in millis used by sentry-native. Default is 5000 = 5s. */ - private long appHangTimeoutIntervalMillis = 5000; + private long ndkAppHangTimeoutIntervalMillis = 5000; /** * Enable or disable automatic breadcrumbs for Activity lifecycle. Using @@ -360,8 +360,8 @@ public void setAnrReportInDebug(boolean anrReportInDebug) { * @return true if enabled or false otherwise */ @ApiStatus.Experimental - public boolean isEnableAppHangTracking() { - return enableAppHangTracking; + public boolean isEnableNdkAppHangTracking() { + return enableNdkAppHangTracking; } /** @@ -369,11 +369,11 @@ public boolean isEnableAppHangTracking() { * Requires the NDK integration to be present and emitting heartbeats via the native {@code * sentry_app_hang_heartbeat()}. * - * @param enableAppHangTracking true for enabled and false for disabled + * @param enableNdkAppHangTracking true for enabled and false for disabled */ @ApiStatus.Experimental - public void setEnableAppHangTracking(boolean enableAppHangTracking) { - this.enableAppHangTracking = enableAppHangTracking; + public void setEnableNdkAppHangTracking(boolean enableNdkAppHangTracking) { + this.enableNdkAppHangTracking = enableNdkAppHangTracking; } /** @@ -383,19 +383,19 @@ public void setEnableAppHangTracking(boolean enableAppHangTracking) { * @return the timeout in millis */ @ApiStatus.Experimental - public long getAppHangTimeoutIntervalMillis() { - return appHangTimeoutIntervalMillis; + public long getNdkAppHangTimeoutIntervalMillis() { + return ndkAppHangTimeoutIntervalMillis; } /** * Sets the app-hang detection timeout interval in millis used by sentry-native. Default is 5000 = * 5s. * - * @param appHangTimeoutIntervalMillis the timeout interval in millis + * @param ndkAppHangTimeoutIntervalMillis the timeout interval in millis */ @ApiStatus.Experimental - public void setAppHangTimeoutIntervalMillis(long appHangTimeoutIntervalMillis) { - this.appHangTimeoutIntervalMillis = appHangTimeoutIntervalMillis; + public void setNdkAppHangTimeoutIntervalMillis(long ndkAppHangTimeoutIntervalMillis) { + this.ndkAppHangTimeoutIntervalMillis = ndkAppHangTimeoutIntervalMillis; } /** 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 0345b5885e2..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 @@ -291,14 +291,14 @@ class ManifestMetadataReaderTest { @Test fun `applyMetadata reads app hang tracking enabled to options`() { // Arrange - val bundle = bundleOf(ManifestMetadataReader.APP_HANG_TRACKING_ENABLE to true) + 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.isEnableAppHangTracking) + assertEquals(true, fixture.options.isEnableNdkAppHangTracking) } @Test @@ -310,20 +310,20 @@ class ManifestMetadataReaderTest { ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertEquals(false, fixture.options.isEnableAppHangTracking) + assertEquals(false, fixture.options.isEnableNdkAppHangTracking) } @Test fun `applyMetadata reads app hang timeout interval to options`() { // Arrange - val bundle = bundleOf(ManifestMetadataReader.APP_HANG_TIMEOUT_INTERVAL_MILLIS to 1000) + 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.appHangTimeoutIntervalMillis) + assertEquals(1000.toLong(), fixture.options.ndkAppHangTimeoutIntervalMillis) } @Test @@ -335,7 +335,7 @@ class ManifestMetadataReaderTest { ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertEquals(5000.toLong(), fixture.options.appHangTimeoutIntervalMillis) + assertEquals(5000.toLong(), fixture.options.ndkAppHangTimeoutIntervalMillis) } @Test 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 fc08bc9cf91..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 @@ -242,8 +242,8 @@ class SentryAndroidOptionsTest { @Test fun `app hang tracking is disabled by default with a 5s timeout`() { val sentryOptions = SentryAndroidOptions() - assertFalse(sentryOptions.isEnableAppHangTracking) - assertEquals(5000L, sentryOptions.appHangTimeoutIntervalMillis) + assertFalse(sentryOptions.isEnableNdkAppHangTracking) + assertEquals(5000L, sentryOptions.ndkAppHangTimeoutIntervalMillis) } private class CustomDebugImagesLoader : IDebugImagesLoader { diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java index 4d37e282da1..fae652f7a64 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java @@ -73,8 +73,8 @@ public static void init(@NotNull final SentryAndroidOptions options) { ndkOptions.setTracesSampleRate(tracesSampleRate.floatValue()); } - ndkOptions.setEnableAppHangTracking(options.isEnableAppHangTracking()); - ndkOptions.setAppHangTimeoutMillis(options.getAppHangTimeoutIntervalMillis()); + ndkOptions.setEnableAppHangTracking(options.isEnableNdkAppHangTracking()); + ndkOptions.setAppHangTimeoutMillis(options.getNdkAppHangTimeoutIntervalMillis()); //noinspection UnstableApiUsage io.sentry.ndk.SentryNdk.init(ndkOptions); diff --git a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/SentryNdkTest.kt b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/SentryNdkTest.kt index f5c1ca64518..d9b332ac185 100644 --- a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/SentryNdkTest.kt +++ b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/SentryNdkTest.kt @@ -87,8 +87,8 @@ class SentryNdkTest { SentryAndroidOptions().apply { dsn = "https://key@sentry.io/proj" cacheDirPath = "/cache" - isEnableAppHangTracking = true - appHangTimeoutIntervalMillis = 2000 + isEnableNdkAppHangTracking = true + ndkAppHangTimeoutIntervalMillis = 2000 } ) { assertNotNull(fixture.capturedOptions) From a38ddae42d6ccbbca872bc81739653d0f2dfc6b0 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 6 Jul 2026 11:43:04 +0200 Subject: [PATCH 5/7] fixed changelog --- CHANGELOG.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 980e4b822c4..161e7853a33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ - 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`) -<<<<<<< HEAD ### Fixes - Record byte-level client reports when event processors discard logs or trace metrics ([#5718](https://github.com/getsentry/sentry-java/pull/5718)) @@ -18,11 +17,6 @@ ## 8.47.0 -||||||| merged common ancestors -||||||||| 57d359a2de -========= -======= ->>>>>>> 770d0555406ff9636e14bb6fd07186e5e6b3e9e3 ### Behavioral Changes - `SentryOkHttpInterceptor::intercept` now throws `IOException`. This is a source-only and Java-only breaking change ([#5654](https://github.com/getsentry/sentry-java/pull/5654)) From b98ab2d3eaae47763a646e4195f36afd4f8aa9eb Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 6 Jul 2026 12:00:25 +0200 Subject: [PATCH 6/7] client tests --- .../test/java/io/sentry/SentryClientTest.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 5b407bfef3d..ea64675570d 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -649,6 +649,52 @@ class SentryClientTest { } } + @Test + fun `when event is cached, scope breadcrumbs are not merged into the event breadcrumbs`() { + val scopeCrumb = Breadcrumb(DateUtils.getDateTime("2020-03-27T08:52:58.001Z")) + val scope = Scope(SentryOptions()).apply { addBreadcrumb(scopeCrumb) } + + val sut = fixture.getSut() + + val eventCrumb = Breadcrumb(DateUtils.getDateTime("2020-03-27T08:52:58.003Z")) + val event = SentryEvent().apply { breadcrumbs = mutableListOf(eventCrumb) } + + val hints = HintUtils.createWithTypeCheckHint(CachedHint()) + sut.captureEvent(event, scope, hints) + + assertNotNull(event.breadcrumbs) { + assertEquals(1, it.size) + assertSame(eventCrumb, it[0]) + } + } + + @Test + fun `when transaction is cached, scope breadcrumbs are not merged into the transaction breadcrumbs`() { + val sut = fixture.getSut() + val scope = Scope(fixture.sentryOptions) + scope.addBreadcrumb(Breadcrumb("from scope")) + + val transaction = SentryTransaction(fixture.sentryTracer) + transaction.breadcrumbs = mutableListOf(Breadcrumb("from transaction")) + + val hints = HintUtils.createWithTypeCheckHint(CachedHint()) + sut.captureTransaction(transaction, scope, hints) + + verify(fixture.transport) + .send( + check { envelope -> + val sent = envelope.items.first().getTransaction(fixture.sentryOptions.serializer) + assertNotNull(sent) { + assertNotNull(it.breadcrumbs) { breadcrumbs -> + assertEquals(1, breadcrumbs.size) + assertEquals("from transaction", breadcrumbs.first().message) + } + } + }, + anyOrNull(), + ) + } + @Test fun `when captureEvent with scope, event data has priority over scope but level and it should append extras, tags and breadcrumbs`() { val event = createEvent() From d3a61274af79e4c157856573c941fb4a2aa41106 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 6 Jul 2026 18:59:14 +0200 Subject: [PATCH 7/7] bumped ndk to 0.15.3 --- CHANGELOG.md | 6 ++++++ gradle/libs.versions.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 161e7853a33..f78af36b4fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ - Name the device-info caching thread `SentryDeviceInfoCache` so all threads spawned by the SDK are identifiable ([#5684](https://github.com/getsentry/sentry-java/pull/5684)) - Apply byte-category rate limits to log and trace metric envelope items ([#5716](https://github.com/getsentry/sentry-java/pull/5716)) +### Dependencies + +- Bump Native SDK from v0.15.2 to v0.15.3 ([#5623](https://github.com/getsentry/sentry-java/pull/5623)) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0153) + - [diff](https://github.com/getsentry/sentry-native/compare/0.15.2...0.15.3) + ## 8.47.0 ### Behavioral Changes diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a145040e7c..e153eb8bc4a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -166,7 +166,7 @@ quartz = { module = "org.quartz-scheduler:quartz", version = "2.3.0" } reactor-core = { module = "io.projectreactor:reactor-core", version = "3.5.3" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } -sentry-native-ndk = { module = "io.sentry:sentry-native-ndk", version = "0.15.2" } +sentry-native-ndk = { module = "io.sentry:sentry-native-ndk", version = "0.15.3" } servlet-api = { module = "javax.servlet:javax.servlet-api", version = "3.1.0" } servlet-jakarta-api = { module = "jakarta.servlet:jakarta.servlet-api", version = "6.1.0" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }