Skip to content

Commit c10e603

Browse files
authored
Merge e92a82b into 6727e14
2 parents 6727e14 + e92a82b commit c10e603

33 files changed

+2459
-39
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
### Features
66

77
- Add `installGroupsOverride` parameter to Build Distribution SDK for programmatic filtering, with support for configuration via properties file using `io.sentry.distribution.install-groups-override` ([#5066](https://github.com/getsentry/sentry-java/pull/5066))
8+
- Add new experimental option to capture profiles for ANRs ([#4899](https://github.com/getsentry/sentry-java/pull/4899))
9+
- This feature will capture a stack profile of the main thread when it gets unresponsive
10+
- 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
11+
- 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
12+
- Enable via `options.setEnableAnrProfiling(true)` or Android manifest: `<meta-data android:name="io.sentry.anr.enable-profiling" android:value="true" />`
813

914
### Dependencies
1015

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

109-
### Feature
114+
### Features
110115

111116
- 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))
112117
- Currently exposed via options as an _internal_ API only.

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
364364
public fun isCollectExternalStorageContext ()Z
365365
public fun isEnableActivityLifecycleBreadcrumbs ()Z
366366
public fun isEnableActivityLifecycleTracingAutoFinish ()Z
367+
public fun isEnableAnrProfiling ()Z
367368
public fun isEnableAppComponentBreadcrumbs ()Z
368369
public fun isEnableAppLifecycleBreadcrumbs ()Z
369370
public fun isEnableAutoActivityLifecycleTracing ()Z
@@ -392,6 +393,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
392393
public fun setDebugImagesLoader (Lio/sentry/android/core/IDebugImagesLoader;)V
393394
public fun setEnableActivityLifecycleBreadcrumbs (Z)V
394395
public fun setEnableActivityLifecycleTracingAutoFinish (Z)V
396+
public fun setEnableAnrProfiling (Z)V
395397
public fun setEnableAppComponentBreadcrumbs (Z)V
396398
public fun setEnableAppLifecycleBreadcrumbs (Z)V
397399
public fun setEnableAutoActivityLifecycleTracing (Z)V
@@ -546,6 +548,84 @@ public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentr
546548
public static fun snapshotViewHierarchyAsData (Landroid/app/Activity;Lio/sentry/util/thread/IThreadChecker;Lio/sentry/ISerializer;Lio/sentry/ILogger;)[B
547549
}
548550

551+
public class io/sentry/android/core/anr/AggregatedStackTrace {
552+
public fun <init> ([Ljava/lang/StackTraceElement;IIJF)V
553+
public fun addOccurrence (J)V
554+
public fun getStack ()[Ljava/lang/StackTraceElement;
555+
}
556+
557+
public class io/sentry/android/core/anr/AnrCulpritIdentifier {
558+
public fun <init> ()V
559+
public static fun identify (Ljava/util/List;)Lio/sentry/android/core/anr/AggregatedStackTrace;
560+
public static fun isSystemFrame (Ljava/lang/String;)Z
561+
}
562+
563+
public class io/sentry/android/core/anr/AnrException : java/lang/Exception {
564+
public fun <init> ()V
565+
public fun <init> (Ljava/lang/String;)V
566+
}
567+
568+
public class io/sentry/android/core/anr/AnrProfile {
569+
public final field endtimeMs J
570+
public final field stacks Ljava/util/List;
571+
public final field startTimeMs J
572+
public fun <init> (Ljava/util/List;)V
573+
}
574+
575+
public class io/sentry/android/core/anr/AnrProfileManager : java/io/Closeable {
576+
public fun <init> (Lio/sentry/SentryOptions;)V
577+
public fun <init> (Lio/sentry/SentryOptions;Ljava/io/File;)V
578+
public fun add (Lio/sentry/android/core/anr/AnrStackTrace;)V
579+
public fun clear ()V
580+
public fun close ()V
581+
public fun load ()Lio/sentry/android/core/anr/AnrProfile;
582+
}
583+
584+
public class io/sentry/android/core/anr/AnrProfileRotationHelper {
585+
public fun <init> ()V
586+
public static fun deleteLastFile (Ljava/io/File;)Z
587+
public static fun getFileForRecording (Ljava/io/File;)Ljava/io/File;
588+
public static fun getLastFile (Ljava/io/File;)Ljava/io/File;
589+
public static fun rotate ()V
590+
}
591+
592+
public class io/sentry/android/core/anr/AnrProfilingIntegration : io/sentry/Integration, io/sentry/android/core/AppState$AppStateListener, java/io/Closeable, java/lang/Runnable {
593+
public static final field POLLING_INTERVAL_MS J
594+
public static final field THRESHOLD_ANR_MS J
595+
public fun <init> ()V
596+
protected fun checkMainThread (Ljava/lang/Thread;)V
597+
public fun close ()V
598+
protected fun getProfileManager ()Lio/sentry/android/core/anr/AnrProfileManager;
599+
protected fun getState ()Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
600+
public fun onBackground ()V
601+
public fun onForeground ()V
602+
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
603+
public fun run ()V
604+
}
605+
606+
protected final class io/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState : java/lang/Enum {
607+
public static final field ANR_DETECTED Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
608+
public static final field IDLE Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
609+
public static final field SUSPICIOUS Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
610+
public static fun valueOf (Ljava/lang/String;)Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
611+
public static fun values ()[Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
612+
}
613+
614+
public final class io/sentry/android/core/anr/AnrStackTrace : java/lang/Comparable {
615+
public final field stack [Ljava/lang/StackTraceElement;
616+
public final field timestampMs J
617+
public fun <init> (J[Ljava/lang/StackTraceElement;)V
618+
public fun compareTo (Lio/sentry/android/core/anr/AnrStackTrace;)I
619+
public synthetic fun compareTo (Ljava/lang/Object;)I
620+
public static fun deserialize (Ljava/io/DataInputStream;)Lio/sentry/android/core/anr/AnrStackTrace;
621+
public fun serialize (Ljava/io/DataOutputStream;)V
622+
}
623+
624+
public final class io/sentry/android/core/anr/StackTraceConverter {
625+
public fun <init> ()V
626+
public static fun convert (Lio/sentry/android/core/anr/AnrProfile;)Lio/sentry/protocol/profiling/SentryProfile;
627+
}
628+
549629
public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry/cache/EnvelopeCache {
550630
public static final field LAST_ANR_MARKER_LABEL Ljava/lang/String;
551631
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;
@@ -140,6 +142,8 @@ static void loadDefaultAndMetadataOptions(
140142
.getRuntimeManager()
141143
.runWithRelaxedPolicy(() -> getCacheDir(finalContext).getAbsolutePath()));
142144

145+
AnrProfileRotationHelper.rotate();
146+
143147
readDefaultOptionValues(options, finalContext, buildInfoProvider);
144148
AppState.getInstance().registerLifecycleObserver(options);
145149
}
@@ -400,6 +404,8 @@ static void installDefaultIntegrations(
400404
// it to set the replayId in case of an ANR
401405
options.addIntegration(AnrIntegrationFactory.create(context, buildInfoProvider));
402406

407+
options.addIntegration(new AnrProfilingIntegration());
408+
403409
// registerActivityLifecycleCallbacks is only available if Context is an AppContext
404410
if (context instanceof Application) {
405411
options.addIntegration(

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) {
8686
.getExecutorService()
8787
.submit(
8888
new ApplicationExitInfoHistoryDispatcher(
89-
context, scopes, this.options, dateProvider, new AnrV2Policy(this.options)));
89+
context,
90+
scopes,
91+
this.options,
92+
dateProvider,
93+
new AnrV2Policy(scopes, this.options)));
9094
} catch (Throwable e) {
9195
options.getLogger().log(SentryLevel.DEBUG, "Failed to start ANR processor.", e);
9296
}
@@ -105,9 +109,11 @@ public void close() throws IOException {
105109
private static final class AnrV2Policy
106110
implements ApplicationExitInfoHistoryDispatcher.ApplicationExitInfoPolicy {
107111

112+
private final @NotNull IScopes scopes;
108113
private final @NotNull SentryAndroidOptions options;
109114

110-
AnrV2Policy(final @NotNull SentryAndroidOptions options) {
115+
AnrV2Policy(final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options) {
116+
this.scopes = scopes;
111117
this.options = options;
112118
}
113119

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

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,23 @@
2929
import io.sentry.Breadcrumb;
3030
import io.sentry.Hint;
3131
import io.sentry.IpAddressUtils;
32+
import io.sentry.ProfileChunk;
33+
import io.sentry.ProfileContext;
34+
import io.sentry.Sentry;
3235
import io.sentry.SentryBaseEvent;
3336
import io.sentry.SentryEvent;
3437
import io.sentry.SentryExceptionFactory;
3538
import io.sentry.SentryLevel;
3639
import io.sentry.SentryOptions;
3740
import io.sentry.SentryStackTraceFactory;
3841
import io.sentry.SpanContext;
42+
import io.sentry.android.core.anr.AggregatedStackTrace;
43+
import io.sentry.android.core.anr.AnrCulpritIdentifier;
44+
import io.sentry.android.core.anr.AnrException;
45+
import io.sentry.android.core.anr.AnrProfile;
46+
import io.sentry.android.core.anr.AnrProfileManager;
47+
import io.sentry.android.core.anr.AnrProfileRotationHelper;
48+
import io.sentry.android.core.anr.StackTraceConverter;
3949
import io.sentry.android.core.internal.util.CpuInfoUtils;
4050
import io.sentry.cache.PersistingOptionsObserver;
4151
import io.sentry.cache.PersistingScopeObserver;
@@ -50,10 +60,14 @@
5060
import io.sentry.protocol.OperatingSystem;
5161
import io.sentry.protocol.Request;
5262
import io.sentry.protocol.SdkVersion;
63+
import io.sentry.protocol.SentryException;
64+
import io.sentry.protocol.SentryId;
65+
import io.sentry.protocol.SentryStackFrame;
5366
import io.sentry.protocol.SentryStackTrace;
5467
import io.sentry.protocol.SentryThread;
5568
import io.sentry.protocol.SentryTransaction;
5669
import io.sentry.protocol.User;
70+
import io.sentry.protocol.profiling.SentryProfile;
5771
import io.sentry.util.HintUtils;
5872
import io.sentry.util.SentryRandom;
5973
import java.io.File;
@@ -700,8 +714,15 @@ public void applyPreEnrichment(
700714
public void applyPostEnrichment(
701715
@NotNull SentryEvent event, @NotNull Backfillable hint, @NotNull Object rawHint) {
702716
final boolean isBackgroundAnr = isBackgroundAnr(rawHint);
703-
setAppForeground(event, !isBackgroundAnr);
717+
718+
if (options.isEnableAnrProfiling()) {
719+
applyAnrProfile(event, hint, isBackgroundAnr);
720+
}
721+
704722
setDefaultAnrFingerprint(event, isBackgroundAnr);
723+
724+
// Set app foreground state
725+
setAppForeground(event, !isBackgroundAnr);
705726
}
706727

707728
private void setDefaultAnrFingerprint(
@@ -710,6 +731,14 @@ private void setDefaultAnrFingerprint(
710731
// so we're doing this on the SDK side to group background and foreground ANRs separately
711732
// even if they have similar stacktraces.
712733
if (event.getFingerprints() == null) {
734+
if (options.isEnableAnrProfiling() && hasOnlySystemFrames(event)) {
735+
// If profiling did not identify any app frames, we want to statically group these events
736+
// to avoid ANR noise due to {{ default }} stacktrace grouping
737+
event.setFingerprints(
738+
Arrays.asList(
739+
"system-frames-only-anr", isBackgroundAnr ? "background-anr" : "foreground-anr"));
740+
return;
741+
}
713742
event.setFingerprints(
714743
Arrays.asList("{{ default }}", isBackgroundAnr ? "background-anr" : "foreground-anr"));
715744
}
@@ -777,5 +806,136 @@ private void setAnrExceptions(
777806
event.setExceptions(
778807
sentryExceptionFactory.getSentryExceptionsFromThread(mainThread, mechanism, anr));
779808
}
809+
810+
private void applyAnrProfile(
811+
@NotNull SentryEvent event, @NotNull Backfillable hint, boolean isBackgroundAnr) {
812+
813+
// Skip background ANRs (profiling only runs in foreground)
814+
if (isBackgroundAnr) {
815+
return;
816+
}
817+
818+
// Get timestamp from hint (ANR hints implement AbnormalExit which has timestamp())
819+
if (!(hint instanceof AbnormalExit)) {
820+
return;
821+
}
822+
final Long anrTimestampObj = ((AbnormalExit) hint).timestamp();
823+
final long anrTimestamp;
824+
if (anrTimestampObj != null) {
825+
anrTimestamp = anrTimestampObj;
826+
} else {
827+
anrTimestamp = event.getTimestamp().getTime();
828+
}
829+
830+
// Read profile from disk
831+
final String cacheDirPath = options.getCacheDirPath();
832+
if (cacheDirPath == null) {
833+
return;
834+
}
835+
final File cacheDir = new File(cacheDirPath);
836+
837+
AnrProfile anrProfile = null;
838+
try {
839+
final File lastFile = AnrProfileRotationHelper.getLastFile(cacheDir);
840+
if (lastFile.exists()) {
841+
options.getLogger().log(SentryLevel.DEBUG, "Reading ANR profile");
842+
try (final AnrProfileManager provider = new AnrProfileManager(options, lastFile)) {
843+
anrProfile = provider.load();
844+
}
845+
} else {
846+
options.getLogger().log(SentryLevel.DEBUG, "No ANR profile file found");
847+
}
848+
} catch (Throwable t) {
849+
options.getLogger().log(SentryLevel.INFO, "Could not retrieve ANR profile", t);
850+
} finally {
851+
if (!AnrProfileRotationHelper.deleteLastFile(cacheDir)) {
852+
options.getLogger().log(SentryLevel.INFO, "Could not delete ANR profile file");
853+
}
854+
}
855+
856+
if (anrProfile == null) {
857+
return;
858+
}
859+
860+
// Validate timestamp
861+
options.getLogger().log(SentryLevel.INFO, "ANR profile found");
862+
if (anrTimestamp < anrProfile.startTimeMs || anrTimestamp > anrProfile.endtimeMs) {
863+
options.getLogger().log(SentryLevel.DEBUG, "ANR profile found, but doesn't match");
864+
return;
865+
}
866+
867+
// Identify culprit
868+
final AggregatedStackTrace culprit = AnrCulpritIdentifier.identify(anrProfile.stacks);
869+
if (culprit == null) {
870+
return;
871+
}
872+
873+
// Capture profile chunk
874+
final SentryId profilerId = captureAnrProfile(anrTimestamp, anrProfile);
875+
876+
// Set exceptions with culprit
877+
final StackTraceElement[] stack = culprit.getStack();
878+
if (stack.length > 0) {
879+
final StackTraceElement stackTraceElement = stack[0];
880+
final String message =
881+
stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName();
882+
final AnrException exception = new AnrException(message);
883+
exception.setStackTrace(stack);
884+
885+
event.setExceptions(sentryExceptionFactory.getSentryExceptions(exception));
886+
887+
if (profilerId != null) {
888+
event.getContexts().setProfile(new ProfileContext(profilerId));
889+
}
890+
}
891+
}
892+
893+
@Nullable
894+
private SentryId captureAnrProfile(final long anrTimestampMs, @NotNull AnrProfile anrProfile) {
895+
final SentryProfile profile = StackTraceConverter.convert(anrProfile);
896+
final ProfileChunk chunk =
897+
new ProfileChunk(
898+
new SentryId(),
899+
new SentryId(),
900+
null,
901+
new HashMap<>(0),
902+
anrTimestampMs / 1000.0d,
903+
ProfileChunk.PLATFORM_JAVA,
904+
options);
905+
chunk.setSentryProfile(profile);
906+
907+
final SentryId profilerId = Sentry.getCurrentScopes().captureProfileChunk(chunk);
908+
if (SentryId.EMPTY_ID.equals(profilerId)) {
909+
return null;
910+
} else {
911+
return chunk.getProfilerId();
912+
}
913+
}
914+
915+
private boolean hasOnlySystemFrames(@NotNull SentryEvent event) {
916+
final List<SentryException> exceptions = event.getExceptions();
917+
if (exceptions == null || exceptions.isEmpty()) {
918+
return false;
919+
}
920+
921+
for (final SentryException exception : exceptions) {
922+
final SentryStackTrace stacktrace = exception.getStacktrace();
923+
if (stacktrace != null) {
924+
final List<SentryStackFrame> frames = stacktrace.getFrames();
925+
if (frames != null && !frames.isEmpty()) {
926+
for (final SentryStackFrame frame : frames) {
927+
if (frame.isInApp() != null && frame.isInApp()) {
928+
return false;
929+
}
930+
final String module = frame.getModule();
931+
if (module != null && !AnrCulpritIdentifier.isSystemFrame(module)) {
932+
return false;
933+
}
934+
}
935+
}
936+
}
937+
}
938+
return true;
939+
}
780940
}
781941
}

0 commit comments

Comments
 (0)