Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DebugImage>? = null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
ndkOptions.setTracesSampleRate(tracesSampleRate.floatValue());
}

ndkOptions.setEnableAppHangTracking(options.isEnableNdkAppHangTracking());

Check failure on line 76 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / Benchmarks

cannot find symbol

Check failure on line 76 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / AGP Matrix Release - AGP 8.8.0 - Integrations true

cannot find symbol

Check failure on line 76 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / AGP Matrix Release - AGP 8.7.0 - Integrations true

cannot find symbol

Check failure on line 76 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / app-metrics

cannot find symbol

Check failure on line 76 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / AGP Matrix Release - AGP 8.9.0 - Integrations true

cannot find symbol

Check failure on line 76 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / Ui tests

cannot find symbol

Check failure on line 76 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / Build and Analyze SDK Size

cannot find symbol

Check failure on line 76 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / Build Job ubuntu-latest - Java 17

cannot find symbol
ndkOptions.setAppHangTimeoutMillis(options.getNdkAppHangTimeoutIntervalMillis());

Check failure on line 77 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / Benchmarks

cannot find symbol

Check failure on line 77 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / AGP Matrix Release - AGP 8.8.0 - Integrations true

cannot find symbol

Check failure on line 77 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / AGP Matrix Release - AGP 8.7.0 - Integrations true

cannot find symbol

Check failure on line 77 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / app-metrics

cannot find symbol

Check failure on line 77 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / AGP Matrix Release - AGP 8.9.0 - Integrations true

cannot find symbol

Check failure on line 77 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / Ui tests

cannot find symbol

Check failure on line 77 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / Build and Analyze SDK Size

cannot find symbol

Check failure on line 77 in sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java

View workflow job for this annotation

GitHub Actions / Build Job ubuntu-latest - Java 17

cannot find symbol

//noinspection UnstableApiUsage
io.sentry.ndk.SentryNdk.init(ndkOptions);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
isEnableNdkAppHangTracking = true
ndkAppHangTimeoutIntervalMillis = 2000
}
) {
assertNotNull(fixture.capturedOptions)
assertTrue(fixture.capturedOptions!!.isEnableAppHangTracking)
assertEquals(2000L, fixture.capturedOptions!!.appHangTimeoutMillis)
}
}
}
10 changes: 6 additions & 4 deletions sentry/src/main/java/io/sentry/SentryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -992,7 +992,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());
Expand Down Expand Up @@ -1407,7 +1407,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());
Expand Down Expand Up @@ -1539,7 +1539,7 @@ public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metri
}

private <T extends SentryBaseEvent> @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());
Expand All @@ -1558,7 +1558,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) {
Expand Down
46 changes: 46 additions & 0 deletions sentry/src/test/java/io/sentry/SentryClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading