diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2052048bbc0..046d36280df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,7 +37,8 @@
- This feature will capture a stack profile of the main thread when it gets unresponsive
- The profile gets attached to the ANR event on the next app start, providing a flamegraph of the ANR issue on the sentry issue details page
- Breaking change: if the ANR stacktrace contains only system frames (e.g. `java.lang` or `android.os`), a static fingerprint is set on the ANR event, causing all ANR events to be grouped into a single issue, reducing the overall ANR issue noise
- - Enable via `options.setEnableAnrProfiling(true)` or Android manifest: ``
+ - Enable via `options.setAnrProfilingSampleRate()` or AndroidManifest.xml: ``
+ - The sample rate controls the probability of collecting a profile for each detected foreground ANR (0.0 to 1.0, null to disable)
### Fixes
diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api
index 60e0547cf3d..c3607ddbcff 100644
--- a/sentry-android-core/api/sentry-android-core.api
+++ b/sentry-android-core/api/sentry-android-core.api
@@ -347,6 +347,7 @@ public final class io/sentry/android/core/SentryAndroidDateProvider : io/sentry/
public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/SentryOptions {
public fun ()V
public fun enableAllAutoBreadcrumbs (Z)V
+ public fun getAnrProfilingSampleRate ()Ljava/lang/Double;
public fun getAnrTimeoutIntervalMillis ()J
public fun getBeforeScreenshotCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;
public fun getBeforeViewHierarchyCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;
@@ -357,6 +358,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun getScreenshot ()Lio/sentry/android/core/SentryScreenshotOptions;
public fun getStartupCrashDurationThresholdMillis ()J
public fun isAnrEnabled ()Z
+ public fun isAnrProfilingEnabled ()Z
public fun isAnrReportInDebug ()Z
public fun isAttachAnrThreadDump ()Z
public fun isAttachScreenshot ()Z
@@ -365,7 +367,6 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun isCollectExternalStorageContext ()Z
public fun isEnableActivityLifecycleBreadcrumbs ()Z
public fun isEnableActivityLifecycleTracingAutoFinish ()Z
- public fun isEnableAnrProfiling ()Z
public fun isEnableAppComponentBreadcrumbs ()Z
public fun isEnableAppLifecycleBreadcrumbs ()Z
public fun isEnableAutoActivityLifecycleTracing ()Z
@@ -382,6 +383,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun isReportHistoricalTombstones ()Z
public fun isTombstoneEnabled ()Z
public fun setAnrEnabled (Z)V
+ public fun setAnrProfilingSampleRate (Ljava/lang/Double;)V
public fun setAnrReportInDebug (Z)V
public fun setAnrTimeoutIntervalMillis (J)V
public fun setAttachAnrThreadDump (Z)V
@@ -394,7 +396,6 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun setDebugImagesLoader (Lio/sentry/android/core/IDebugImagesLoader;)V
public fun setEnableActivityLifecycleBreadcrumbs (Z)V
public fun setEnableActivityLifecycleTracingAutoFinish (Z)V
- public fun setEnableAnrProfiling (Z)V
public fun setEnableAppComponentBreadcrumbs (Z)V
public fun setEnableAppLifecycleBreadcrumbs (Z)V
public fun setEnableAutoActivityLifecycleTracing (Z)V
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java
index 382972ce101..a031920d720 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java
@@ -715,7 +715,7 @@ public void applyPostEnrichment(
@NotNull SentryEvent event, @NotNull Backfillable hint, @NotNull Object rawHint) {
final boolean isBackgroundAnr = isBackgroundAnr(rawHint);
- if (options.isEnableAnrProfiling()) {
+ if (options.isAnrProfilingEnabled()) {
applyAnrProfile(event, hint, isBackgroundAnr);
}
@@ -734,7 +734,7 @@ private void setDefaultAnrFingerprint(
return;
}
- if (options.isEnableAnrProfiling() && hasOnlySystemFrames(event)) {
+ if (options.isAnrProfilingEnabled() && hasOnlySystemFrames(event)) {
// If profiling did not identify any app frames, we want to statically group these events
// to avoid ANR noise due to {{ default }} stacktrace grouping
event.setFingerprints(
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 6e60b953806..47189e7813b 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
@@ -175,7 +175,7 @@ final class ManifestMetadataReader {
static final String SCREENSHOT_MASK_ALL_IMAGES = "io.sentry.screenshot.mask-all-images";
- static final String ENABLE_ANR_PROFILING = "io.sentry.anr.profiling.enable";
+ static final String ANR_PROFILING_SAMPLE_RATE = "io.sentry.anr.profiling.sample-rate";
/** ManifestMetadataReader ctor */
private ManifestMetadataReader() {}
@@ -677,8 +677,13 @@ static void applyMetadata(
.getScreenshot()
.setMaskAllImages(readBool(metadata, logger, SCREENSHOT_MASK_ALL_IMAGES, false));
- options.setEnableAnrProfiling(
- readBool(metadata, logger, ENABLE_ANR_PROFILING, options.isEnableAnrProfiling()));
+ if (options.getAnrProfilingSampleRate() == null) {
+ final double anrProfilingSampleRate =
+ readDouble(metadata, logger, ANR_PROFILING_SAMPLE_RATE);
+ if (anrProfilingSampleRate != -1) {
+ options.setAnrProfilingSampleRate(anrProfilingSampleRate);
+ }
+ }
}
options
.getLogger()
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 3a6269fbc63..a2034574fcb 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
@@ -17,6 +17,7 @@
import io.sentry.protocol.Mechanism;
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryId;
+import io.sentry.util.SampleRateUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -252,7 +253,7 @@ public interface BeforeCaptureCallback {
*/
private final @NotNull SentryScreenshotOptions screenshot = new SentryScreenshotOptions();
- private boolean enableAnrProfiling = false;
+ private @Nullable Double anrProfilingSampleRate;
public SentryAndroidOptions() {
setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
@@ -697,12 +698,22 @@ public void setEnableSystemEventBreadcrumbsExtras(
return screenshot;
}
- public boolean isEnableAnrProfiling() {
- return enableAnrProfiling;
+ public @Nullable Double getAnrProfilingSampleRate() {
+ return anrProfilingSampleRate;
}
- public void setEnableAnrProfiling(final boolean enableAnrProfiling) {
- this.enableAnrProfiling = enableAnrProfiling;
+ public void setAnrProfilingSampleRate(final @Nullable Double anrProfilingSampleRate) {
+ if (!SampleRateUtils.isValidSampleRate(anrProfilingSampleRate)) {
+ throw new IllegalArgumentException(
+ "The value "
+ + anrProfilingSampleRate
+ + " is not valid. Use null to disable or values >= 0.0 and <= 1.0.");
+ }
+ this.anrProfilingSampleRate = anrProfilingSampleRate;
+ }
+
+ public boolean isAnrProfilingEnabled() {
+ return anrProfilingSampleRate != null && anrProfilingSampleRate > 0;
}
static class AndroidUserFeedbackIDialogHandler implements SentryFeedbackOptions.IDialogHandler {
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfilingIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfilingIntegration.java
index ac67e7b4007..97ec0434249 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfilingIntegration.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfilingIntegration.java
@@ -16,6 +16,7 @@
import io.sentry.android.core.SentryAndroidOptions;
import io.sentry.util.AutoClosableReentrantLock;
import io.sentry.util.Objects;
+import io.sentry.util.SentryRandom;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
@@ -47,6 +48,7 @@ public class AnrProfilingIntegration
private volatile @NotNull ILogger logger = NoOpLogger.getInstance();
private volatile @Nullable SentryAndroidOptions options;
private volatile @Nullable Thread thread = null;
+ private volatile boolean sampled = false;
private volatile boolean inForeground = false;
private volatile @Nullable Handler mainHandler;
private volatile @Nullable Thread mainThread;
@@ -59,7 +61,7 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
"SentryAndroidOptions is required");
this.logger = options.getLogger();
- if (this.options.isEnableAnrProfiling()) {
+ if (this.options.isAnrProfilingEnabled()) {
if (this.options.getCacheDirPath() == null) {
logger.log(SentryLevel.WARNING, "ANR Profiling is enabled but cacheDirPath is not set");
return;
@@ -207,6 +209,7 @@ protected void checkMainThread(final @NotNull Thread mainThread) throws IOExcept
if (diff < THRESHOLD_SUSPICION_MS) {
mainThreadState = MainThreadState.IDLE;
+ sampled = false;
}
if (mainThreadState == MainThreadState.IDLE && diff > THRESHOLD_SUSPICION_MS) {
@@ -214,12 +217,22 @@ protected void checkMainThread(final @NotNull Thread mainThread) throws IOExcept
logger.log(SentryLevel.DEBUG, "ANR: main thread is suspicious");
}
mainThreadState = MainThreadState.SUSPICIOUS;
- clearStacks();
+
+ final @Nullable SentryAndroidOptions opts = options;
+ final @Nullable Double sampleRate = opts != null ? opts.getAnrProfilingSampleRate() : null;
+ if (sampleRate != null && SentryRandom.current().nextDouble() < sampleRate) {
+ sampled = true;
+ }
+
+ if (sampled) {
+ clearStacks();
+ }
}
- // if we are suspicious, we need to collect stack traces
- if (mainThreadState == MainThreadState.SUSPICIOUS
- || mainThreadState == MainThreadState.ANR_DETECTED) {
+ // if we are suspicious and sampled, we need to collect stack traces
+ if (sampled
+ && (mainThreadState == MainThreadState.SUSPICIOUS
+ || mainThreadState == MainThreadState.ANR_DETECTED)) {
if (numCollectedStacks.get() < MAX_NUM_STACKS) {
final long start = SystemClock.uptimeMillis();
final @NotNull AnrStackTrace trace =
diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt
index 0d012652ac1..a7995772541 100644
--- a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt
+++ b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt
@@ -637,7 +637,7 @@ class ApplicationExitInfoEventProcessorTest {
@Test
fun `sets system-frames-only fingerprint when ANR profiling enabled and no app frames`() {
- fixture.options.isEnableAnrProfiling = true
+ fixture.options.anrProfilingSampleRate = 1.0
val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint(mechanism = "anr_foreground"))
val processed =
@@ -666,7 +666,7 @@ class ApplicationExitInfoEventProcessorTest {
@Test
fun `does not set system-frames-only fingerprint when ANR profiling is disabled but no app frames are present`() {
- fixture.options.isEnableAnrProfiling = false
+ fixture.options.anrProfilingSampleRate = null
val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint(mechanism = "anr_foreground"))
val processed =
@@ -695,7 +695,7 @@ class ApplicationExitInfoEventProcessorTest {
@Test
fun `sets default fingerprint when ANR profiling enabled and app frames are present`() {
- fixture.options.isEnableAnrProfiling = true
+ fixture.options.anrProfilingSampleRate = 1.0
val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint(mechanism = "anr_foreground"))
val processed =
@@ -723,7 +723,7 @@ class ApplicationExitInfoEventProcessorTest {
@Test
fun `does not set profile context when ANR profiling is disabled`() {
- fixture.options.isEnableAnrProfiling = false
+ fixture.options.anrProfilingSampleRate = null
val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint(mechanism = "anr_foreground"))
val processed =
processEvent(hint, populateScopeCache = false) {
@@ -749,7 +749,7 @@ class ApplicationExitInfoEventProcessorTest {
@Test
fun `applies ANR profile if available`() {
- fixture.options.isEnableAnrProfiling = true
+ fixture.options.anrProfilingSampleRate = 1.0
val processor =
fixture.getSut(
tmpDir,
@@ -798,7 +798,7 @@ class ApplicationExitInfoEventProcessorTest {
@Test
fun `does not crash when ANR profiling is enabled but cache dir is null`() {
- fixture.options.isEnableAnrProfiling = true
+ fixture.options.anrProfilingSampleRate = 1.0
fixture.options.cacheDirPath = null
val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint(mechanism = "anr_foreground"))
val original = SentryEvent()
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 ee1905113ff..f257a5d09e6 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
@@ -1998,20 +1998,20 @@ class ManifestMetadataReaderTest {
}
@Test
- fun `applyMetadata reads enableAnrProfiling to options`() {
+ fun `applyMetadata reads anrProfilingSampleRate to options`() {
// Arrange
- val bundle = bundleOf(ManifestMetadataReader.ENABLE_ANR_PROFILING to true)
+ val bundle = bundleOf(ManifestMetadataReader.ANR_PROFILING_SAMPLE_RATE to 0.5f)
val context = fixture.getContext(metaData = bundle)
// Act
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
// Assert
- assertTrue(fixture.options.isEnableAnrProfiling)
+ assertEquals(0.5, fixture.options.anrProfilingSampleRate!!, 0.01)
}
@Test
- fun `applyMetadata reads enableAnrProfiling to options and keeps default`() {
+ fun `applyMetadata keeps anrProfilingSampleRate default when not set in manifest`() {
// Arrange
val context = fixture.getContext()
@@ -2019,7 +2019,7 @@ class ManifestMetadataReaderTest {
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
// Assert
- assertFalse(fixture.options.isEnableAnrProfiling)
+ assertNull(fixture.options.anrProfilingSampleRate)
}
// Network Detail Configuration Tests
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 8c9e8b3152c..819928dcdc4 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
@@ -196,25 +196,41 @@ class SentryAndroidOptionsTest {
}
@Test
- fun `anr profiling disabled by default`() {
+ fun `anr profiling sample rate is null by default`() {
val sentryOptions = SentryAndroidOptions()
- assertFalse(sentryOptions.isEnableAnrProfiling)
+ assertNull(sentryOptions.anrProfilingSampleRate)
+ assertFalse(sentryOptions.isAnrProfilingEnabled)
}
@Test
- fun `anr profiling can be enabled`() {
+ fun `anr profiling can be enabled via sample rate`() {
val sentryOptions = SentryAndroidOptions()
- sentryOptions.isEnableAnrProfiling = true
- assertTrue(sentryOptions.isEnableAnrProfiling)
+ sentryOptions.anrProfilingSampleRate = 1.0
+ assertEquals(1.0, sentryOptions.anrProfilingSampleRate)
+ assertTrue(sentryOptions.isAnrProfilingEnabled)
}
@Test
- fun `anr profiling can be disabled`() {
+ fun `anr profiling can be disabled via null sample rate`() {
val sentryOptions = SentryAndroidOptions()
- sentryOptions.isEnableAnrProfiling = true
- sentryOptions.isEnableAnrProfiling = false
- assertFalse(sentryOptions.isEnableAnrProfiling)
+ sentryOptions.anrProfilingSampleRate = 1.0
+ sentryOptions.anrProfilingSampleRate = null
+ assertNull(sentryOptions.anrProfilingSampleRate)
+ assertFalse(sentryOptions.isAnrProfilingEnabled)
+ }
+
+ @Test
+ fun `anr profiling is disabled when sample rate is zero`() {
+ val sentryOptions = SentryAndroidOptions()
+ sentryOptions.anrProfilingSampleRate = 0.0
+ assertFalse(sentryOptions.isAnrProfilingEnabled)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `anr profiling rejects invalid sample rate`() {
+ val sentryOptions = SentryAndroidOptions()
+ sentryOptions.anrProfilingSampleRate = 2.0
}
private class CustomDebugImagesLoader : IDebugImagesLoader {
diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/anr/AnrProfilingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/anr/AnrProfilingIntegrationTest.kt
index fb7af8219c5..c07bb4d71bb 100644
--- a/sentry-android-core/src/test/java/io/sentry/android/core/anr/AnrProfilingIntegrationTest.kt
+++ b/sentry-android-core/src/test/java/io/sentry/android/core/anr/AnrProfilingIntegrationTest.kt
@@ -39,7 +39,7 @@ class AnrProfilingIntegrationTest {
SentryAndroidOptions().apply {
cacheDirPath = tmpDir.root.absolutePath
setLogger(mockLogger)
- isEnableAnrProfiling = true
+ anrProfilingSampleRate = 1.0
}
AppState.getInstance().resetInstance()
}
@@ -169,7 +169,7 @@ class AnrProfilingIntegrationTest {
SentryAndroidOptions().apply {
cacheDirPath = tmpDir.root.absolutePath
setLogger(mockLogger)
- isEnableAnrProfiling = true
+ anrProfilingSampleRate = 1.0
}
val integration = AnrProfilingIntegration()
@@ -205,7 +205,7 @@ class AnrProfilingIntegrationTest {
SentryAndroidOptions().apply {
cacheDirPath = tmpDir.root.absolutePath
setLogger(mockLogger)
- isEnableAnrProfiling = true
+ anrProfilingSampleRate = 1.0
}
val integration = AnrProfilingIntegration()
@@ -249,7 +249,7 @@ class AnrProfilingIntegrationTest {
SentryAndroidOptions().apply {
cacheDirPath = tmpDir.root.absolutePath
setLogger(mockLogger)
- isEnableAnrProfiling = false
+ anrProfilingSampleRate = null
}
val integration = AnrProfilingIntegration()
@@ -263,13 +263,43 @@ class AnrProfilingIntegrationTest {
}
}
+ @Test
+ fun `does not collect stacks when sample rate is zero`() {
+ val mainThread = Thread.currentThread()
+ SystemClock.setCurrentTimeMillis(1_00)
+
+ val androidOptions =
+ SentryAndroidOptions().apply {
+ cacheDirPath = tmpDir.root.absolutePath
+ setLogger(mockLogger)
+ anrProfilingSampleRate = 0.0
+ }
+
+ val integration = AnrProfilingIntegration()
+ integration.register(mockScopes, androidOptions)
+ integration.onForeground()
+
+ // Transition to suspicious
+ SystemClock.setCurrentTimeMillis(3_000)
+ integration.checkMainThread(mainThread)
+ assertEquals(AnrProfilingIntegration.MainThreadState.SUSPICIOUS, integration.state)
+
+ // Transition to ANR
+ SystemClock.setCurrentTimeMillis(6_000)
+ integration.checkMainThread(mainThread)
+ assertEquals(AnrProfilingIntegration.MainThreadState.ANR_DETECTED, integration.state)
+
+ // No stacks should have been collected
+ assertEquals(0, integration.numCollectedStacks.get())
+ }
+
@Test
fun `registers when ANR profiling is enabled`() {
val androidOptions =
SentryAndroidOptions().apply {
cacheDirPath = tmpDir.root.absolutePath
setLogger(mockLogger)
- isEnableAnrProfiling = true
+ anrProfilingSampleRate = 1.0
}
val integration = AnrProfilingIntegration()
diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml
index 399ce4fc5d2..c6c39dfdb63 100644
--- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml
+++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml
@@ -266,7 +266,7 @@
android:name="io.sentry.tombstone.enable"
android:value="true" />
+ android:name="io.sentry.anr.profiling.sample-rate"
+ android:value="1.0" />