Skip to content

Commit d501a7e

Browse files
markushiclauderomtsn
authored
feat(anr): Profile main thread when ANR and report ANR profiles to Sentry (#4899)
* Profile main thread when ANR and report ANR profiles to sentry * docs(changelog): Add ANR profiling integration entry 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix api dump file * Address PR feedback * refactor(anr): Implement lazy file rotation for ANR profiling * Update Changelog * Address PR feedback * Improve folding logic, cleanup tests * Add more tests and address feedback * Update CHANGELOG.md * Address PR feedcback * Move logic to event processor * Update changelog * Ensure integration is tracked * Address PR feedback * Fix tests * Match manifest property to convention, enable profiling in sample app * Add more bound checks and null guards * Remove outdated meta-data * Properly handle foreground transitions * Address PR comments * Address PR feedback * Address PR feedback * Address PR feedback * Re-use thread * Update Changelop * Address review * Address PR feedback * Replace ANR profiling boolean flag with sample-rate (#5156) * feat(android): Add enableAnrFingerprinting option (#5168) * feat(android): Add enableAnrFingerprinting option Decouple ANR fingerprinting from ANR profiling into a standalone opt-in option. This allows static fingerprinting of system-frame-only ANRs regardless of whether profiling is enabled. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(android): Mark enableAnrFingerprinting as experimental Also enable the option in the sample app. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(android): Default enableAnrFingerprinting to true Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(android): Remove experimental flag from enableAnrFingerprinting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix api dump * Address PR feedback --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Fix tests --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Roman Zavarnitsyn <rom4ek93@gmail.com>
1 parent b67bb28 commit d501a7e

37 files changed

+2772
-58
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@
4545
<meta-data android:name="io.sentry.screenshot.mask-all-images" android:value="true" />
4646
```
4747
- The `ManifestMetaDataReader` now read the `DIST` ([#5107](https://github.com/getsentry/sentry-java/pull/5107))
48+
- Add new experimental option to capture profiles for ANRs ([#4899](https://github.com/getsentry/sentry-java/pull/4899))
49+
- This feature will capture a stack profile of the main thread when it gets unresponsive
50+
- 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
51+
- 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]" />`
52+
- The sample rate controls the probability of collecting a profile for each detected foreground ANR (0.0 to 1.0, null to disable)
53+
- Add `enableAnrFingerprinting` option to reduce ANR noise by assigning static fingerprints to ANR events with system-only stacktraces
54+
- When enabled, ANRs whose stacktraces contain only system frames (e.g. `java.lang` or `android.os`) are grouped into a single issue instead of creating many separate issues
55+
- Enable via `options.setEnableAnrFingerprinting(true)` or AndroidManifest.xml: `<meta-data android:name="io.sentry.anr.enable-fingerprinting" android:value="true" />`
4856

4957
### Fixes
5058

@@ -180,7 +188,7 @@
180188
- Discard envelopes on `4xx` and `5xx` response ([#4950](https://github.com/getsentry/sentry-java/pull/4950))
181189
- This aims to not overwhelm Sentry after an outage or load shedding (including HTTP 429) where too many events are sent at once
182190

183-
### Feature
191+
### Features
184192

185193
- Add a Tombstone integration that detects native crashes without relying on the NDK integration, but instead using `ApplicationExitInfo.REASON_CRASH_NATIVE` on Android 12+. ([#4933](https://github.com/getsentry/sentry-java/pull/4933))
186194
- Currently exposed via options as an _internal_ API only.

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

Lines changed: 78 additions & 0 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,6 +367,7 @@ 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
370+
public fun isEnableAnrFingerprinting ()Z
368371
public fun isEnableAppComponentBreadcrumbs ()Z
369372
public fun isEnableAppLifecycleBreadcrumbs ()Z
370373
public fun isEnableAutoActivityLifecycleTracing ()Z
@@ -381,6 +384,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
381384
public fun isReportHistoricalTombstones ()Z
382385
public fun isTombstoneEnabled ()Z
383386
public fun setAnrEnabled (Z)V
387+
public fun setAnrProfilingSampleRate (Ljava/lang/Double;)V
384388
public fun setAnrReportInDebug (Z)V
385389
public fun setAnrTimeoutIntervalMillis (J)V
386390
public fun setAttachAnrThreadDump (Z)V
@@ -393,6 +397,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
393397
public fun setDebugImagesLoader (Lio/sentry/android/core/IDebugImagesLoader;)V
394398
public fun setEnableActivityLifecycleBreadcrumbs (Z)V
395399
public fun setEnableActivityLifecycleTracingAutoFinish (Z)V
400+
public fun setEnableAnrFingerprinting (Z)V
396401
public fun setEnableAppComponentBreadcrumbs (Z)V
397402
public fun setEnableAppLifecycleBreadcrumbs (Z)V
398403
public fun setEnableAutoActivityLifecycleTracing (Z)V
@@ -553,6 +558,79 @@ public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentr
553558
public static fun snapshotViewHierarchyAsData (Landroid/app/Activity;Lio/sentry/util/thread/IThreadChecker;Lio/sentry/ISerializer;Lio/sentry/ILogger;)[B
554559
}
555560

561+
public class io/sentry/android/core/anr/AggregatedStackTrace {
562+
public fun <init> ([Ljava/lang/StackTraceElement;IIJF)V
563+
public fun addOccurrence (J)V
564+
public fun getStack ()[Ljava/lang/StackTraceElement;
565+
}
566+
567+
public class io/sentry/android/core/anr/AnrCulpritIdentifier {
568+
public fun <init> ()V
569+
public static fun identify (Ljava/util/List;)Lio/sentry/android/core/anr/AggregatedStackTrace;
570+
public static fun isSystemFrame (Ljava/lang/String;)Z
571+
}
572+
573+
public class io/sentry/android/core/anr/AnrProfile {
574+
public final field endTimeMs J
575+
public final field stacks Ljava/util/List;
576+
public final field startTimeMs J
577+
public fun <init> (Ljava/util/List;)V
578+
}
579+
580+
public class io/sentry/android/core/anr/AnrProfileManager : java/lang/AutoCloseable {
581+
public fun <init> (Lio/sentry/SentryOptions;)V
582+
public fun <init> (Lio/sentry/SentryOptions;Ljava/io/File;)V
583+
public fun add (Lio/sentry/android/core/anr/AnrStackTrace;)V
584+
public fun clear ()V
585+
public fun close ()V
586+
public fun load ()Lio/sentry/android/core/anr/AnrProfile;
587+
}
588+
589+
public class io/sentry/android/core/anr/AnrProfileRotationHelper {
590+
public fun <init> ()V
591+
public static fun deleteLastFile (Ljava/io/File;)Z
592+
public static fun getFileForRecording (Ljava/io/File;)Ljava/io/File;
593+
public static fun getLastFile (Ljava/io/File;)Ljava/io/File;
594+
public static fun rotate ()V
595+
}
596+
597+
public class io/sentry/android/core/anr/AnrProfilingIntegration : io/sentry/Integration, io/sentry/android/core/AppState$AppStateListener, java/io/Closeable, java/lang/Runnable {
598+
public static final field POLLING_INTERVAL_MS J
599+
public static final field THRESHOLD_ANR_MS J
600+
public fun <init> ()V
601+
protected fun checkMainThread (Ljava/lang/Thread;)V
602+
public fun close ()V
603+
protected fun getProfileManager ()Lio/sentry/android/core/anr/AnrProfileManager;
604+
protected fun getState ()Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
605+
public fun onBackground ()V
606+
public fun onForeground ()V
607+
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
608+
public fun run ()V
609+
}
610+
611+
protected final class io/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState : java/lang/Enum {
612+
public static final field ANR_DETECTED Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
613+
public static final field IDLE Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
614+
public static final field SUSPICIOUS Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
615+
public static fun valueOf (Ljava/lang/String;)Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
616+
public static fun values ()[Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
617+
}
618+
619+
public final class io/sentry/android/core/anr/AnrStackTrace : java/lang/Comparable {
620+
public final field stack [Ljava/lang/StackTraceElement;
621+
public final field timestampMs J
622+
public fun <init> (J[Ljava/lang/StackTraceElement;)V
623+
public fun compareTo (Lio/sentry/android/core/anr/AnrStackTrace;)I
624+
public synthetic fun compareTo (Ljava/lang/Object;)I
625+
public static fun deserialize (Ljava/io/DataInputStream;)Lio/sentry/android/core/anr/AnrStackTrace;
626+
public fun serialize (Ljava/io/DataOutputStream;)V
627+
}
628+
629+
public final class io/sentry/android/core/anr/StackTraceConverter {
630+
public fun <init> ()V
631+
public static fun convert (Lio/sentry/android/core/anr/AnrProfile;)Lio/sentry/protocol/profiling/SentryProfile;
632+
}
633+
556634
public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry/cache/EnvelopeCache {
557635
public static final field LAST_ANR_MARKER_LABEL Ljava/lang/String;
558636
public static final field LAST_ANR_REPORT Ljava/lang/String;

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import io.sentry.SendFireAndForgetOutboxSender;
2727
import io.sentry.SentryLevel;
2828
import io.sentry.SentryOpenTelemetryMode;
29+
import io.sentry.android.core.anr.AnrProfileRotationHelper;
30+
import io.sentry.android.core.anr.AnrProfilingIntegration;
2931
import io.sentry.android.core.cache.AndroidEnvelopeCache;
3032
import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader;
3133
import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator;
@@ -135,6 +137,8 @@ static void loadDefaultAndMetadataOptions(
135137

136138
options.setCacheDirPath(getCacheDir(finalContext).getAbsolutePath());
137139

140+
AnrProfileRotationHelper.rotate();
141+
138142
readDefaultOptionValues(options, finalContext, buildInfoProvider);
139143
AppState.getInstance().registerLifecycleObserver(options);
140144
options.activate();
@@ -397,6 +401,8 @@ static void installDefaultIntegrations(
397401
// it to set the replayId in case of an ANR
398402
options.addIntegration(AnrIntegrationFactory.create(context, buildInfoProvider));
399403

404+
options.addIntegration(new AnrProfilingIntegration());
405+
400406
// registerActivityLifecycleCallbacks is only available if Context is an AppContext
401407
if (context instanceof Application) {
402408
options.addIntegration(

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,18 @@ void reportANR(
139139
message = "Background " + message;
140140
}
141141

142-
final ApplicationNotResponding error = new ApplicationNotResponding(message, anr.getThread());
142+
final @Nullable Thread thread = anr.getThread();
143+
final @NotNull ApplicationNotResponding error;
144+
if (thread == null) {
145+
error = new ApplicationNotResponding(message);
146+
} else {
147+
error = new ApplicationNotResponding(message, thread);
148+
}
149+
143150
final Mechanism mechanism = new Mechanism();
144151
mechanism.setType("ANR");
145152

146-
return new ExceptionMechanismException(mechanism, error, error.getThread(), true);
153+
return new ExceptionMechanismException(mechanism, error, thread, true);
147154
}
148155

149156
@TestOnly

0 commit comments

Comments
 (0)