Skip to content

Commit 034445f

Browse files
authored
Replace ANR profiling boolean flag with sample-rate (#5156)
1 parent 0cc4bba commit 034445f

File tree

11 files changed

+122
-45
lines changed

11 files changed

+122
-45
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
- This feature will capture a stack profile of the main thread when it gets unresponsive
3838
- 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
3939
- 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
40-
- Enable via `options.setEnableAnrProfiling(true)` or Android manifest: `<meta-data android:name="io.sentry.anr.profiling.enable" android:value="true" />`
40+
- Enable via `options.setAnrProfilingSampleRate(<sample-rate>)` or AndroidManifest.xml: `<meta-data android:name="io.sentry.anr.profiling.sample-rate" android:value="[0.0-1.0]" />`
41+
- The sample rate controls the probability of collecting a profile for each detected foreground ANR (0.0 to 1.0, null to disable)
4142

4243
### Fixes
4344

sentry-android-core/api/sentry-android-core.api

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ public final class io/sentry/android/core/SentryAndroidDateProvider : io/sentry/
347347
public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/SentryOptions {
348348
public fun <init> ()V
349349
public fun enableAllAutoBreadcrumbs (Z)V
350+
public fun getAnrProfilingSampleRate ()Ljava/lang/Double;
350351
public fun getAnrTimeoutIntervalMillis ()J
351352
public fun getBeforeScreenshotCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;
352353
public fun getBeforeViewHierarchyCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;
@@ -357,6 +358,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
357358
public fun getScreenshot ()Lio/sentry/android/core/SentryScreenshotOptions;
358359
public fun getStartupCrashDurationThresholdMillis ()J
359360
public fun isAnrEnabled ()Z
361+
public fun isAnrProfilingEnabled ()Z
360362
public fun isAnrReportInDebug ()Z
361363
public fun isAttachAnrThreadDump ()Z
362364
public fun isAttachScreenshot ()Z
@@ -365,7 +367,6 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
365367
public fun isCollectExternalStorageContext ()Z
366368
public fun isEnableActivityLifecycleBreadcrumbs ()Z
367369
public fun isEnableActivityLifecycleTracingAutoFinish ()Z
368-
public fun isEnableAnrProfiling ()Z
369370
public fun isEnableAppComponentBreadcrumbs ()Z
370371
public fun isEnableAppLifecycleBreadcrumbs ()Z
371372
public fun isEnableAutoActivityLifecycleTracing ()Z
@@ -382,6 +383,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
382383
public fun isReportHistoricalTombstones ()Z
383384
public fun isTombstoneEnabled ()Z
384385
public fun setAnrEnabled (Z)V
386+
public fun setAnrProfilingSampleRate (Ljava/lang/Double;)V
385387
public fun setAnrReportInDebug (Z)V
386388
public fun setAnrTimeoutIntervalMillis (J)V
387389
public fun setAttachAnrThreadDump (Z)V
@@ -394,7 +396,6 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
394396
public fun setDebugImagesLoader (Lio/sentry/android/core/IDebugImagesLoader;)V
395397
public fun setEnableActivityLifecycleBreadcrumbs (Z)V
396398
public fun setEnableActivityLifecycleTracingAutoFinish (Z)V
397-
public fun setEnableAnrProfiling (Z)V
398399
public fun setEnableAppComponentBreadcrumbs (Z)V
399400
public fun setEnableAppLifecycleBreadcrumbs (Z)V
400401
public fun setEnableAutoActivityLifecycleTracing (Z)V

sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,7 @@ public void applyPostEnrichment(
715715
@NotNull SentryEvent event, @NotNull Backfillable hint, @NotNull Object rawHint) {
716716
final boolean isBackgroundAnr = isBackgroundAnr(rawHint);
717717

718-
if (options.isEnableAnrProfiling()) {
718+
if (options.isAnrProfilingEnabled()) {
719719
applyAnrProfile(event, hint, isBackgroundAnr);
720720
}
721721

@@ -734,7 +734,7 @@ private void setDefaultAnrFingerprint(
734734
return;
735735
}
736736

737-
if (options.isEnableAnrProfiling() && hasOnlySystemFrames(event)) {
737+
if (options.isAnrProfilingEnabled() && hasOnlySystemFrames(event)) {
738738
// If profiling did not identify any app frames, we want to statically group these events
739739
// to avoid ANR noise due to {{ default }} stacktrace grouping
740740
event.setFingerprints(

sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ final class ManifestMetadataReader {
175175

176176
static final String SCREENSHOT_MASK_ALL_IMAGES = "io.sentry.screenshot.mask-all-images";
177177

178-
static final String ENABLE_ANR_PROFILING = "io.sentry.anr.profiling.enable";
178+
static final String ANR_PROFILING_SAMPLE_RATE = "io.sentry.anr.profiling.sample-rate";
179179

180180
/** ManifestMetadataReader ctor */
181181
private ManifestMetadataReader() {}
@@ -677,8 +677,13 @@ static void applyMetadata(
677677
.getScreenshot()
678678
.setMaskAllImages(readBool(metadata, logger, SCREENSHOT_MASK_ALL_IMAGES, false));
679679

680-
options.setEnableAnrProfiling(
681-
readBool(metadata, logger, ENABLE_ANR_PROFILING, options.isEnableAnrProfiling()));
680+
if (options.getAnrProfilingSampleRate() == null) {
681+
final double anrProfilingSampleRate =
682+
readDouble(metadata, logger, ANR_PROFILING_SAMPLE_RATE);
683+
if (anrProfilingSampleRate != -1) {
684+
options.setAnrProfilingSampleRate(anrProfilingSampleRate);
685+
}
686+
}
682687
}
683688
options
684689
.getLogger()

sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import io.sentry.protocol.Mechanism;
1818
import io.sentry.protocol.SdkVersion;
1919
import io.sentry.protocol.SentryId;
20+
import io.sentry.util.SampleRateUtils;
2021
import org.jetbrains.annotations.ApiStatus;
2122
import org.jetbrains.annotations.NotNull;
2223
import org.jetbrains.annotations.Nullable;
@@ -252,7 +253,7 @@ public interface BeforeCaptureCallback {
252253
*/
253254
private final @NotNull SentryScreenshotOptions screenshot = new SentryScreenshotOptions();
254255

255-
private boolean enableAnrProfiling = false;
256+
private @Nullable Double anrProfilingSampleRate;
256257

257258
public SentryAndroidOptions() {
258259
setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
@@ -697,12 +698,22 @@ public void setEnableSystemEventBreadcrumbsExtras(
697698
return screenshot;
698699
}
699700

700-
public boolean isEnableAnrProfiling() {
701-
return enableAnrProfiling;
701+
public @Nullable Double getAnrProfilingSampleRate() {
702+
return anrProfilingSampleRate;
702703
}
703704

704-
public void setEnableAnrProfiling(final boolean enableAnrProfiling) {
705-
this.enableAnrProfiling = enableAnrProfiling;
705+
public void setAnrProfilingSampleRate(final @Nullable Double anrProfilingSampleRate) {
706+
if (!SampleRateUtils.isValidSampleRate(anrProfilingSampleRate)) {
707+
throw new IllegalArgumentException(
708+
"The value "
709+
+ anrProfilingSampleRate
710+
+ " is not valid. Use null to disable or values >= 0.0 and <= 1.0.");
711+
}
712+
this.anrProfilingSampleRate = anrProfilingSampleRate;
713+
}
714+
715+
public boolean isAnrProfilingEnabled() {
716+
return anrProfilingSampleRate != null && anrProfilingSampleRate > 0;
706717
}
707718

708719
static class AndroidUserFeedbackIDialogHandler implements SentryFeedbackOptions.IDialogHandler {

sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfilingIntegration.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import io.sentry.android.core.SentryAndroidOptions;
1717
import io.sentry.util.AutoClosableReentrantLock;
1818
import io.sentry.util.Objects;
19+
import io.sentry.util.SentryRandom;
1920
import java.io.Closeable;
2021
import java.io.File;
2122
import java.io.IOException;
@@ -47,6 +48,7 @@ public class AnrProfilingIntegration
4748
private volatile @NotNull ILogger logger = NoOpLogger.getInstance();
4849
private volatile @Nullable SentryAndroidOptions options;
4950
private volatile @Nullable Thread thread = null;
51+
private volatile boolean sampled = false;
5052
private volatile boolean inForeground = false;
5153
private volatile @Nullable Handler mainHandler;
5254
private volatile @Nullable Thread mainThread;
@@ -59,7 +61,7 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
5961
"SentryAndroidOptions is required");
6062
this.logger = options.getLogger();
6163

62-
if (this.options.isEnableAnrProfiling()) {
64+
if (this.options.isAnrProfilingEnabled()) {
6365
if (this.options.getCacheDirPath() == null) {
6466
logger.log(SentryLevel.WARNING, "ANR Profiling is enabled but cacheDirPath is not set");
6567
return;
@@ -207,19 +209,30 @@ protected void checkMainThread(final @NotNull Thread mainThread) throws IOExcept
207209

208210
if (diff < THRESHOLD_SUSPICION_MS) {
209211
mainThreadState = MainThreadState.IDLE;
212+
sampled = false;
210213
}
211214

212215
if (mainThreadState == MainThreadState.IDLE && diff > THRESHOLD_SUSPICION_MS) {
213216
if (logger.isEnabled(SentryLevel.DEBUG)) {
214217
logger.log(SentryLevel.DEBUG, "ANR: main thread is suspicious");
215218
}
216219
mainThreadState = MainThreadState.SUSPICIOUS;
217-
clearStacks();
220+
221+
final @Nullable SentryAndroidOptions opts = options;
222+
final @Nullable Double sampleRate = opts != null ? opts.getAnrProfilingSampleRate() : null;
223+
if (sampleRate != null && SentryRandom.current().nextDouble() < sampleRate) {
224+
sampled = true;
225+
}
226+
227+
if (sampled) {
228+
clearStacks();
229+
}
218230
}
219231

220-
// if we are suspicious, we need to collect stack traces
221-
if (mainThreadState == MainThreadState.SUSPICIOUS
222-
|| mainThreadState == MainThreadState.ANR_DETECTED) {
232+
// if we are suspicious and sampled, we need to collect stack traces
233+
if (sampled
234+
&& (mainThreadState == MainThreadState.SUSPICIOUS
235+
|| mainThreadState == MainThreadState.ANR_DETECTED)) {
223236
if (numCollectedStacks.get() < MAX_NUM_STACKS) {
224237
final long start = SystemClock.uptimeMillis();
225238
final @NotNull AnrStackTrace trace =

sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ class ApplicationExitInfoEventProcessorTest {
637637

638638
@Test
639639
fun `sets system-frames-only fingerprint when ANR profiling enabled and no app frames`() {
640-
fixture.options.isEnableAnrProfiling = true
640+
fixture.options.anrProfilingSampleRate = 1.0
641641
val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint(mechanism = "anr_foreground"))
642642

643643
val processed =
@@ -666,7 +666,7 @@ class ApplicationExitInfoEventProcessorTest {
666666

667667
@Test
668668
fun `does not set system-frames-only fingerprint when ANR profiling is disabled but no app frames are present`() {
669-
fixture.options.isEnableAnrProfiling = false
669+
fixture.options.anrProfilingSampleRate = null
670670
val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint(mechanism = "anr_foreground"))
671671

672672
val processed =
@@ -695,7 +695,7 @@ class ApplicationExitInfoEventProcessorTest {
695695

696696
@Test
697697
fun `sets default fingerprint when ANR profiling enabled and app frames are present`() {
698-
fixture.options.isEnableAnrProfiling = true
698+
fixture.options.anrProfilingSampleRate = 1.0
699699
val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint(mechanism = "anr_foreground"))
700700

701701
val processed =
@@ -723,7 +723,7 @@ class ApplicationExitInfoEventProcessorTest {
723723

724724
@Test
725725
fun `does not set profile context when ANR profiling is disabled`() {
726-
fixture.options.isEnableAnrProfiling = false
726+
fixture.options.anrProfilingSampleRate = null
727727
val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint(mechanism = "anr_foreground"))
728728
val processed =
729729
processEvent(hint, populateScopeCache = false) {
@@ -749,7 +749,7 @@ class ApplicationExitInfoEventProcessorTest {
749749

750750
@Test
751751
fun `applies ANR profile if available`() {
752-
fixture.options.isEnableAnrProfiling = true
752+
fixture.options.anrProfilingSampleRate = 1.0
753753
val processor =
754754
fixture.getSut(
755755
tmpDir,
@@ -798,7 +798,7 @@ class ApplicationExitInfoEventProcessorTest {
798798

799799
@Test
800800
fun `does not crash when ANR profiling is enabled but cache dir is null`() {
801-
fixture.options.isEnableAnrProfiling = true
801+
fixture.options.anrProfilingSampleRate = 1.0
802802
fixture.options.cacheDirPath = null
803803
val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint(mechanism = "anr_foreground"))
804804
val original = SentryEvent()

sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1998,28 +1998,28 @@ class ManifestMetadataReaderTest {
19981998
}
19991999

20002000
@Test
2001-
fun `applyMetadata reads enableAnrProfiling to options`() {
2001+
fun `applyMetadata reads anrProfilingSampleRate to options`() {
20022002
// Arrange
2003-
val bundle = bundleOf(ManifestMetadataReader.ENABLE_ANR_PROFILING to true)
2003+
val bundle = bundleOf(ManifestMetadataReader.ANR_PROFILING_SAMPLE_RATE to 0.5f)
20042004
val context = fixture.getContext(metaData = bundle)
20052005

20062006
// Act
20072007
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
20082008

20092009
// Assert
2010-
assertTrue(fixture.options.isEnableAnrProfiling)
2010+
assertEquals(0.5, fixture.options.anrProfilingSampleRate!!, 0.01)
20112011
}
20122012

20132013
@Test
2014-
fun `applyMetadata reads enableAnrProfiling to options and keeps default`() {
2014+
fun `applyMetadata keeps anrProfilingSampleRate default when not set in manifest`() {
20152015
// Arrange
20162016
val context = fixture.getContext()
20172017

20182018
// Act
20192019
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
20202020

20212021
// Assert
2022-
assertFalse(fixture.options.isEnableAnrProfiling)
2022+
assertNull(fixture.options.anrProfilingSampleRate)
20232023
}
20242024

20252025
// Network Detail Configuration Tests

sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -196,25 +196,41 @@ class SentryAndroidOptionsTest {
196196
}
197197

198198
@Test
199-
fun `anr profiling disabled by default`() {
199+
fun `anr profiling sample rate is null by default`() {
200200
val sentryOptions = SentryAndroidOptions()
201201

202-
assertFalse(sentryOptions.isEnableAnrProfiling)
202+
assertNull(sentryOptions.anrProfilingSampleRate)
203+
assertFalse(sentryOptions.isAnrProfilingEnabled)
203204
}
204205

205206
@Test
206-
fun `anr profiling can be enabled`() {
207+
fun `anr profiling can be enabled via sample rate`() {
207208
val sentryOptions = SentryAndroidOptions()
208-
sentryOptions.isEnableAnrProfiling = true
209-
assertTrue(sentryOptions.isEnableAnrProfiling)
209+
sentryOptions.anrProfilingSampleRate = 1.0
210+
assertEquals(1.0, sentryOptions.anrProfilingSampleRate)
211+
assertTrue(sentryOptions.isAnrProfilingEnabled)
210212
}
211213

212214
@Test
213-
fun `anr profiling can be disabled`() {
215+
fun `anr profiling can be disabled via null sample rate`() {
214216
val sentryOptions = SentryAndroidOptions()
215-
sentryOptions.isEnableAnrProfiling = true
216-
sentryOptions.isEnableAnrProfiling = false
217-
assertFalse(sentryOptions.isEnableAnrProfiling)
217+
sentryOptions.anrProfilingSampleRate = 1.0
218+
sentryOptions.anrProfilingSampleRate = null
219+
assertNull(sentryOptions.anrProfilingSampleRate)
220+
assertFalse(sentryOptions.isAnrProfilingEnabled)
221+
}
222+
223+
@Test
224+
fun `anr profiling is disabled when sample rate is zero`() {
225+
val sentryOptions = SentryAndroidOptions()
226+
sentryOptions.anrProfilingSampleRate = 0.0
227+
assertFalse(sentryOptions.isAnrProfilingEnabled)
228+
}
229+
230+
@Test(expected = IllegalArgumentException::class)
231+
fun `anr profiling rejects invalid sample rate`() {
232+
val sentryOptions = SentryAndroidOptions()
233+
sentryOptions.anrProfilingSampleRate = 2.0
218234
}
219235

220236
private class CustomDebugImagesLoader : IDebugImagesLoader {

0 commit comments

Comments
 (0)