Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b286ad5
Profile main thread when ANR and report ANR profiles to sentry
markushi Nov 12, 2025
a62b5e8
docs(changelog): Add ANR profiling integration entry
markushi Nov 12, 2025
ae66f73
Merge branch 'main' into markushi/feat/anr-profiling
markushi Nov 12, 2025
f226d84
Fix api dump file
markushi Nov 13, 2025
7d423a4
Address PR feedback
markushi Nov 13, 2025
5824f8f
Merge branch 'markushi/feat/anr-profiling' of github.com:getsentry/se…
markushi Nov 13, 2025
6e7fff2
Merge branch 'main' into markushi/feat/anr-profiling
markushi Dec 1, 2025
3d4d952
refactor(anr): Implement lazy file rotation for ANR profiling
markushi Dec 2, 2025
69170ce
Merge branch 'main' into markushi/feat/anr-profiling
markushi Dec 3, 2025
cee86fc
Update Changelog
markushi Dec 3, 2025
49c2e20
Address PR feedback
markushi Dec 4, 2025
93141dd
Improve folding logic, cleanup tests
markushi Dec 16, 2025
a59bf08
Add more tests and address feedback
markushi Dec 16, 2025
5bfff6a
Merge branch 'main' into markushi/feat/anr-profiling
markushi Dec 16, 2025
6c9acd7
Update CHANGELOG.md
markushi Dec 17, 2025
4c95292
Merge branch 'main' into markushi/feat/anr-profiling
markushi Dec 18, 2025
cdd74b9
Merge branch 'main' into markushi/feat/anr-profiling
markushi Jan 29, 2026
e67c713
Address PR feedcback
markushi Jan 30, 2026
0cd877d
Merge branch 'main' into markushi/feat/anr-profiling
markushi Jan 30, 2026
ff3c01e
Merge branch 'main' into markushi/feat/anr-profiling
markushi Feb 11, 2026
47db30e
Merge branch 'markushi/feat/anr-profiling' of github.com:getsentry/se…
markushi Feb 11, 2026
aefa921
Move logic to event processor
markushi Feb 11, 2026
e68c4cf
Update changelog
markushi Feb 11, 2026
e92a82b
Ensure integration is tracked
markushi Feb 11, 2026
6ecd31e
Address PR feedback
markushi Feb 11, 2026
29b2ff0
Fix tests
markushi Feb 11, 2026
a15602b
Match manifest property to convention, enable profiling in sample app
markushi Feb 11, 2026
d33ccfc
Merge branch 'main' into markushi/feat/anr-profiling
markushi Feb 12, 2026
28c7bfa
Add more bound checks and null guards
markushi Feb 12, 2026
aa79a11
Remove outdated meta-data
markushi Feb 12, 2026
175fc39
Properly handle foreground transitions
markushi Feb 12, 2026
aeb7d72
Address PR comments
markushi Feb 12, 2026
61bb712
Address PR feedback
markushi Feb 13, 2026
39229d5
Address PR feedback
markushi Feb 13, 2026
2ff62db
Address PR feedback
markushi Feb 16, 2026
3d502b8
Re-use thread
markushi Feb 26, 2026
44d8bfa
Merge branch 'main' into markushi/feat/anr-profiling
markushi Feb 26, 2026
cd1f4c2
Update Changelop
markushi Feb 26, 2026
8a49e18
Address review
romtsn Feb 26, 2026
5ae3e11
Address PR feedback
markushi Feb 27, 2026
0cc4bba
Merge branch 'main' into markushi/feat/anr-profiling
markushi Mar 4, 2026
034445f
Replace ANR profiling boolean flag with sample-rate (#5156)
markushi Mar 6, 2026
5e8b866
feat(android): Add enableAnrFingerprinting option (#5168)
markushi Mar 6, 2026
ddbe532
Fix tests
markushi Mar 6, 2026
ab98857
Merge branch 'markushi/feat/anr-profiling' of github.com:getsentry/se…
markushi Mar 6, 2026
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ SentryAndroid.init(
### Improvements

- Do not send manual log origin ([#4897](https://github.com/getsentry/sentry-java/pull/4897))
- Add ANR profiling integration ([#4899](https://github.com/getsentry/sentry-java/pull/4899))
- Captures main thread profile when ANR is detected
- Identifies culprit code causing application hangs
- Profiles are attached to ANR error events for better diagnostics
- Enable via `options.setEnableAnrProfiling(true)` or Android manifest: `<meta-data android:name="io.sentry.anr.enable-profiling" android:value="true" />`


### Dependencies

Expand Down
79 changes: 79 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun isCollectAdditionalContext ()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
Expand All @@ -351,6 +352,7 @@ 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
Expand Down Expand Up @@ -480,6 +482,83 @@ public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentr
public static fun snapshotViewHierarchyAsData (Landroid/app/Activity;Lio/sentry/util/thread/IThreadChecker;Lio/sentry/ISerializer;Lio/sentry/ILogger;)[B
}

public class io/sentry/android/core/anr/AggregatedStackTrace {
public fun <init> ([Ljava/lang/StackTraceElement;IIJI)V
public fun add (J)V
public fun getStack ()[Ljava/lang/StackTraceElement;
}

public class io/sentry/android/core/anr/AnrCulpritIdentifier {
public fun <init> ()V
public static fun identify (Ljava/util/List;)Lio/sentry/android/core/anr/AggregatedStackTrace;
}

public class io/sentry/android/core/anr/AnrException : java/lang/Exception {
public fun <init> ()V
public fun <init> (Ljava/lang/String;)V
}

public class io/sentry/android/core/anr/AnrProfile {
public final field endtimeMs J
public final field stacks Ljava/util/List;
public final field startTimeMs J
public fun <init> (Ljava/util/List;)V
}

public class io/sentry/android/core/anr/AnrProfileManager : java/io/Closeable {
public fun <init> (Lio/sentry/SentryOptions;)V
public fun <init> (Lio/sentry/SentryOptions;Ljava/io/File;)V
public fun add (Lio/sentry/android/core/anr/AnrStackTrace;)V
public fun clear ()V
public fun close ()V
public fun load ()Lio/sentry/android/core/anr/AnrProfile;
}

public class io/sentry/android/core/anr/AnrProfileRotationHelper {
public fun <init> ()V
public static fun deleteLastFile (Ljava/io/File;)Z
public static fun getCurrentFile (Ljava/io/File;)Ljava/io/File;
public static fun getLastFile (Ljava/io/File;)Ljava/io/File;
public static fun rotate ()V
}

public class io/sentry/android/core/anr/AnrProfilingIntegration : io/sentry/Integration, io/sentry/android/core/AppState$AppStateListener, java/io/Closeable, java/lang/Runnable {
public static final field POLLING_INTERVAL_MS J
public static final field THRESHOLD_ANR_MS J
public fun <init> ()V
protected fun checkMainThread (Ljava/lang/Thread;)V
public fun close ()V
protected fun getProfileManager ()Lio/sentry/android/core/anr/AnrProfileManager;
protected fun getState ()Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
public fun onBackground ()V
public fun onForeground ()V
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
public fun run ()V
}

protected final class io/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState : java/lang/Enum {
public static final field ANR_DETECTED Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
public static final field IDLE Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
public static final field SUSPICIOUS Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
public static fun valueOf (Ljava/lang/String;)Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
public static fun values ()[Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
}

public final class io/sentry/android/core/anr/AnrStackTrace : java/lang/Comparable {
public final field stack [Ljava/lang/StackTraceElement;
public final field timestampMs J
public fun <init> (J[Ljava/lang/StackTraceElement;)V
public fun compareTo (Lio/sentry/android/core/anr/AnrStackTrace;)I
public synthetic fun compareTo (Ljava/lang/Object;)I
public static fun deserialize (Ljava/io/DataInputStream;)Lio/sentry/android/core/anr/AnrStackTrace;
public fun serialize (Ljava/io/DataOutputStream;)V
}

public final class io/sentry/android/core/anr/StackTraceConverter {
public fun <init> ()V
public static fun convert (Lio/sentry/android/core/anr/AnrProfile;)Lio/sentry/protocol/profiling/SentryProfile;
}

public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry/cache/EnvelopeCache {
public static final field LAST_ANR_REPORT Ljava/lang/String;
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import io.sentry.SendFireAndForgetOutboxSender;
import io.sentry.SentryLevel;
import io.sentry.SentryOpenTelemetryMode;
import io.sentry.android.core.anr.AnrProfileRotationHelper;
import io.sentry.android.core.anr.AnrProfilingIntegration;
import io.sentry.android.core.cache.AndroidEnvelopeCache;
import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader;
import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator;
Expand Down Expand Up @@ -137,6 +139,8 @@ static void loadDefaultAndMetadataOptions(
.getRuntimeManager()
.runWithRelaxedPolicy(() -> getCacheDir(finalContext).getAbsolutePath()));

AnrProfileRotationHelper.rotate();
Comment thread
romtsn marked this conversation as resolved.

readDefaultOptionValues(options, finalContext, buildInfoProvider);
AppState.getInstance().registerLifecycleObserver(options);
}
Expand Down Expand Up @@ -391,6 +395,10 @@ static void installDefaultIntegrations(
// it to set the replayId in case of an ANR
options.addIntegration(AnrIntegrationFactory.create(context, buildInfoProvider));

if (options.isEnableAnrProfiling()) {
options.addIntegration(new AnrProfilingIntegration());
}

// registerActivityLifecycleCallbacks is only available if Context is an AppContext
if (context instanceof Application) {
options.addIntegration(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,20 @@
import io.sentry.ILogger;
import io.sentry.IScopes;
import io.sentry.Integration;
import io.sentry.ProfileChunk;
import io.sentry.ProfileContext;
import io.sentry.SentryEvent;
import io.sentry.SentryExceptionFactory;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.SentryStackTraceFactory;
import io.sentry.android.core.anr.AggregatedStackTrace;
import io.sentry.android.core.anr.AnrCulpritIdentifier;
import io.sentry.android.core.anr.AnrException;
import io.sentry.android.core.anr.AnrProfile;
import io.sentry.android.core.anr.AnrProfileManager;
import io.sentry.android.core.anr.AnrProfileRotationHelper;
import io.sentry.android.core.anr.StackTraceConverter;
import io.sentry.android.core.cache.AndroidEnvelopeCache;
import io.sentry.android.core.internal.threaddump.Lines;
import io.sentry.android.core.internal.threaddump.ThreadDumpParser;
Expand All @@ -28,6 +39,7 @@
import io.sentry.protocol.Message;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.SentryThread;
import io.sentry.protocol.profiling.SentryProfile;
import io.sentry.transport.CurrentDateProvider;
import io.sentry.transport.ICurrentDateProvider;
import io.sentry.util.HintUtils;
Expand All @@ -36,11 +48,13 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.jetbrains.annotations.ApiStatus;
Expand Down Expand Up @@ -284,6 +298,8 @@ private void reportAsSentryEvent(
}
}

applyAnrProfile(isBackground, anrTimestamp, event);

final @NotNull SentryId sentryId = scopes.captureEvent(event, hint);
final boolean isEventDropped = sentryId.equals(SentryId.EMPTY_ID);
if (!isEventDropped) {
Expand All @@ -299,6 +315,78 @@ private void reportAsSentryEvent(
}
}

private void applyAnrProfile(
final boolean isBackground, final long anrTimestamp, final @NotNull SentryEvent event) {

// as of now AnrProfilingIntegration only generates profiles in foreground
if (isBackground) {
return;
}

@Nullable AnrProfile anrProfile = null;
final File cacheDir = new File(options.getCacheDirPath());

try {
final File lastFile = AnrProfileRotationHelper.getLastFile(cacheDir);

if (lastFile.exists()) {
options.getLogger().log(SentryLevel.DEBUG, "Reading ANR profile from rotated file");
try (final AnrProfileManager provider = new AnrProfileManager(options, lastFile)) {
anrProfile = provider.load();
}
} else {
options.getLogger().log(SentryLevel.DEBUG, "No ANR profile file found");
}
} catch (Throwable t) {
options.getLogger().log(SentryLevel.INFO, "Could not retrieve ANR profile", t);
} finally {
if (AnrProfileRotationHelper.deleteLastFile(cacheDir)) {
options.getLogger().log(SentryLevel.DEBUG, "Deleted old ANR profile file");
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

if (anrProfile != null) {
options.getLogger().log(SentryLevel.INFO, "ANR profile found");
if (anrTimestamp >= anrProfile.startTimeMs && anrTimestamp <= anrProfile.endtimeMs) {
final SentryProfile profile = StackTraceConverter.convert(anrProfile);
final ProfileChunk chunk =
new ProfileChunk(
new SentryId(),
new SentryId(),
null,
new HashMap<>(0),
anrTimestamp / 1000.0d,
ProfileChunk.PLATFORM_JAVA,
options);
chunk.setSentryProfile(profile);
scopes.captureProfileChunk(chunk);

final @Nullable AggregatedStackTrace culprit =
AnrCulpritIdentifier.identify(anrProfile.stacks);
if (culprit != null) {
// TODO Consider setting a static fingerprint to reduce noise
// if culprit quality is low (e.g. when culprit frame is pollNative())
final @NotNull StackTraceElement[] stack = culprit.getStack();
if (stack.length > 0) {
final StackTraceElement stackTraceElement = culprit.getStack()[0];
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
final String message =
stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName();
final AnrException exception = new AnrException(message);
exception.setStackTrace(stack);

// TODO should this be re-used from somewhere else?
final SentryExceptionFactory factory =
new SentryExceptionFactory(new SentryStackTraceFactory(options));
event.setExceptions(factory.getSentryExceptions(exception));
event.getContexts().setProfile(new ProfileContext(chunk.getProfilerId()));
}
}
} else {
options.getLogger().log(SentryLevel.DEBUG, "ANR profile found, but doesn't match");
}
}
}

private @NotNull ParseResult parseThreadDump(
final @NotNull ApplicationExitInfo exitInfo, final boolean isBackground) {
final byte[] dump;
Comment thread
markushi marked this conversation as resolved.
Outdated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ final class ManifestMetadataReader {

static final String FEEDBACK_SHOW_BRANDING = "io.sentry.feedback.show-branding";

static final String ENABLE_ANR_PROFILING = "io.sentry.anr.enable-profiling";

/** ManifestMetadataReader ctor */
private ManifestMetadataReader() {}

Expand Down Expand Up @@ -620,6 +622,9 @@ static void applyMetadata(
metadata, logger, FEEDBACK_USE_SENTRY_USER, feedbackOptions.isUseSentryUser()));
feedbackOptions.setShowBranding(
readBool(metadata, logger, FEEDBACK_SHOW_BRANDING, feedbackOptions.isShowBranding()));

options.setEnableAnrProfiling(
readBool(metadata, logger, ENABLE_ANR_PROFILING, options.isEnableAnrProfiling()));
}
options
.getLogger()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ public interface BeforeCaptureCallback {

private @Nullable SentryFrameMetricsCollector frameMetricsCollector;

private boolean enableAnrProfiling = false;

public SentryAndroidOptions() {
setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
setSdkVersion(createSdkVersion());
Expand Down Expand Up @@ -626,6 +628,14 @@ public void setEnableSystemEventBreadcrumbsExtras(
this.enableSystemEventBreadcrumbsExtras = enableSystemEventBreadcrumbsExtras;
}

public boolean isEnableAnrProfiling() {
return enableAnrProfiling;
}

public void setEnableAnrProfiling(final boolean enableAnrProfiling) {
this.enableAnrProfiling = enableAnrProfiling;
}

static class AndroidUserFeedbackIDialogHandler implements SentryFeedbackOptions.IDialogHandler {
@Override
public void showDialog(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.sentry.android.core.anr;

import java.util.Arrays;
import org.jetbrains.annotations.ApiStatus;

@ApiStatus.Internal
public class AggregatedStackTrace {
// the number of frames of the stacktrace
final int depth;

// the quality of the stack trace, higher means better
final int quality;

private final StackTraceElement[] stack;

// 0 is the most detailed frame in the stacktrace
private final int stackStartIdx;
private final int stackEndIdx;

// the total number of times this exact stacktrace was captured
int count;

// first time the stacktrace occured
private long startTimeMs;

// last time the stacktrace occured
private long endTimeMs;

public AggregatedStackTrace(
final StackTraceElement[] stack,
final int stackStartIdx,
final int stackEndIdx,
final long timestampMs,
final int quality) {
this.stack = stack;
this.stackStartIdx = stackStartIdx;
this.stackEndIdx = stackEndIdx;
this.depth = stackEndIdx - stackStartIdx + 1;
this.startTimeMs = timestampMs;
this.endTimeMs = timestampMs;
this.count = 1;
this.quality = quality;
}

public void add(long timestampMs) {
this.startTimeMs = Math.min(startTimeMs, timestampMs);
this.endTimeMs = Math.max(endTimeMs, timestampMs);
this.count++;
}

public StackTraceElement[] getStack() {
return Arrays.copyOfRange(stack, stackStartIdx, stackEndIdx + 1);
}
}
Loading
Loading