Skip to content

Commit 8c4e7d6

Browse files
authored
Merge cd1f4c2 into 0d66c0b
2 parents 0d66c0b + cd1f4c2 commit 8c4e7d6

37 files changed

+2601
-56
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
<meta-data android:name="io.sentry.screenshot.mask-all-text" android:value="true" />
2424
<meta-data android:name="io.sentry.screenshot.mask-all-images" android:value="true" />
2525
```
26+
- Add new experimental option to capture profiles for ANRs ([#4899](https://github.com/getsentry/sentry-java/pull/4899))
27+
- This feature will capture a stack profile of the main thread when it gets unresponsive
28+
- 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
29+
- 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
30+
- Enable via `options.setEnableAnrProfiling(true)` or Android manifest: `<meta-data android:name="io.sentry.anr.profiling.enable" android:value="true" />`
2631

2732
### Fixes
2833

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

147-
### Feature
152+
### Features
148153

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

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
365365
public fun isCollectExternalStorageContext ()Z
366366
public fun isEnableActivityLifecycleBreadcrumbs ()Z
367367
public fun isEnableActivityLifecycleTracingAutoFinish ()Z
368+
public fun isEnableAnrProfiling ()Z
368369
public fun isEnableAppComponentBreadcrumbs ()Z
369370
public fun isEnableAppLifecycleBreadcrumbs ()Z
370371
public fun isEnableAutoActivityLifecycleTracing ()Z
@@ -393,6 +394,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
393394
public fun setDebugImagesLoader (Lio/sentry/android/core/IDebugImagesLoader;)V
394395
public fun setEnableActivityLifecycleBreadcrumbs (Z)V
395396
public fun setEnableActivityLifecycleTracingAutoFinish (Z)V
397+
public fun setEnableAnrProfiling (Z)V
396398
public fun setEnableAppComponentBreadcrumbs (Z)V
397399
public fun setEnableAppLifecycleBreadcrumbs (Z)V
398400
public fun setEnableAutoActivityLifecycleTracing (Z)V
@@ -553,6 +555,79 @@ public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentr
553555
public static fun snapshotViewHierarchyAsData (Landroid/app/Activity;Lio/sentry/util/thread/IThreadChecker;Lio/sentry/ISerializer;Lio/sentry/ILogger;)[B
554556
}
555557

558+
public class io/sentry/android/core/anr/AggregatedStackTrace {
559+
public fun <init> ([Ljava/lang/StackTraceElement;IIJF)V
560+
public fun addOccurrence (J)V
561+
public fun getStack ()[Ljava/lang/StackTraceElement;
562+
}
563+
564+
public class io/sentry/android/core/anr/AnrCulpritIdentifier {
565+
public fun <init> ()V
566+
public static fun identify (Ljava/util/List;)Lio/sentry/android/core/anr/AggregatedStackTrace;
567+
public static fun isSystemFrame (Ljava/lang/String;)Z
568+
}
569+
570+
public class io/sentry/android/core/anr/AnrProfile {
571+
public final field endTimeMs J
572+
public final field stacks Ljava/util/List;
573+
public final field startTimeMs J
574+
public fun <init> (Ljava/util/List;)V
575+
}
576+
577+
public class io/sentry/android/core/anr/AnrProfileManager : java/lang/AutoCloseable {
578+
public fun <init> (Lio/sentry/SentryOptions;)V
579+
public fun <init> (Lio/sentry/SentryOptions;Ljava/io/File;)V
580+
public fun add (Lio/sentry/android/core/anr/AnrStackTrace;)V
581+
public fun clear ()V
582+
public fun close ()V
583+
public fun load ()Lio/sentry/android/core/anr/AnrProfile;
584+
}
585+
586+
public class io/sentry/android/core/anr/AnrProfileRotationHelper {
587+
public fun <init> ()V
588+
public static fun deleteLastFile (Ljava/io/File;)Z
589+
public static fun getFileForRecording (Ljava/io/File;)Ljava/io/File;
590+
public static fun getLastFile (Ljava/io/File;)Ljava/io/File;
591+
public static fun rotate ()V
592+
}
593+
594+
public class io/sentry/android/core/anr/AnrProfilingIntegration : io/sentry/Integration, io/sentry/android/core/AppState$AppStateListener, java/io/Closeable, java/lang/Runnable {
595+
public static final field POLLING_INTERVAL_MS J
596+
public static final field THRESHOLD_ANR_MS J
597+
public fun <init> ()V
598+
protected fun checkMainThread (Ljava/lang/Thread;)V
599+
public fun close ()V
600+
protected fun getProfileManager ()Lio/sentry/android/core/anr/AnrProfileManager;
601+
protected fun getState ()Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
602+
public fun onBackground ()V
603+
public fun onForeground ()V
604+
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
605+
public fun run ()V
606+
}
607+
608+
protected final class io/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState : java/lang/Enum {
609+
public static final field ANR_DETECTED Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
610+
public static final field IDLE Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
611+
public static final field SUSPICIOUS Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
612+
public static fun valueOf (Ljava/lang/String;)Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
613+
public static fun values ()[Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
614+
}
615+
616+
public final class io/sentry/android/core/anr/AnrStackTrace : java/lang/Comparable {
617+
public final field stack [Ljava/lang/StackTraceElement;
618+
public final field timestampMs J
619+
public fun <init> (J[Ljava/lang/StackTraceElement;)V
620+
public fun compareTo (Lio/sentry/android/core/anr/AnrStackTrace;)I
621+
public synthetic fun compareTo (Ljava/lang/Object;)I
622+
public static fun deserialize (Ljava/io/DataInputStream;)Lio/sentry/android/core/anr/AnrStackTrace;
623+
public fun serialize (Ljava/io/DataOutputStream;)V
624+
}
625+
626+
public final class io/sentry/android/core/anr/StackTraceConverter {
627+
public fun <init> ()V
628+
public static fun convert (Lio/sentry/android/core/anr/AnrProfile;)Lio/sentry/protocol/profiling/SentryProfile;
629+
}
630+
556631
public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry/cache/EnvelopeCache {
557632
public static final field LAST_ANR_MARKER_LABEL Ljava/lang/String;
558633
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
}
@@ -401,6 +405,8 @@ static void installDefaultIntegrations(
401405
// it to set the replayId in case of an ANR
402406
options.addIntegration(AnrIntegrationFactory.create(context, buildInfoProvider));
403407

408+
options.addIntegration(new AnrProfilingIntegration());
409+
404410
// registerActivityLifecycleCallbacks is only available if Context is an AppContext
405411
if (context instanceof Application) {
406412
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, anr.getThread());
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

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

Lines changed: 173 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,26 @@
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.AnrProfile;
45+
import io.sentry.android.core.anr.AnrProfileManager;
46+
import io.sentry.android.core.anr.AnrProfileRotationHelper;
47+
import io.sentry.android.core.anr.StackTraceConverter;
3948
import io.sentry.android.core.internal.util.CpuInfoUtils;
4049
import io.sentry.cache.PersistingOptionsObserver;
4150
import io.sentry.cache.PersistingScopeObserver;
51+
import io.sentry.exception.ExceptionMechanismException;
4252
import io.sentry.hints.AbnormalExit;
4353
import io.sentry.hints.Backfillable;
4454
import io.sentry.protocol.App;
@@ -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,16 +714,33 @@ 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(
708729
final @NotNull SentryEvent event, final boolean isBackgroundAnr) {
709730
// sentry does not yet have a capability to provide default server-side fingerprint rules,
710731
// so we're doing this on the SDK side to group background and foreground ANRs separately
711732
// even if they have similar stacktraces.
712-
if (event.getFingerprints() == null) {
733+
if (event.getFingerprints() != null) {
734+
return;
735+
}
736+
737+
if (options.isEnableAnrProfiling() && hasOnlySystemFrames(event)) {
738+
// If profiling did not identify any app frames, we want to statically group these events
739+
// to avoid ANR noise due to {{ default }} stacktrace grouping
740+
event.setFingerprints(
741+
Arrays.asList(
742+
"system-frames-only-anr", isBackgroundAnr ? "background-anr" : "foreground-anr"));
743+
} else {
713744
event.setFingerprints(
714745
Arrays.asList("{{ default }}", isBackgroundAnr ? "background-anr" : "foreground-anr"));
715746
}
@@ -777,5 +808,145 @@ private void setAnrExceptions(
777808
event.setExceptions(
778809
sentryExceptionFactory.getSentryExceptionsFromThread(mainThread, mechanism, anr));
779810
}
811+
812+
private void applyAnrProfile(
813+
@NotNull SentryEvent event, @NotNull Backfillable hint, boolean isBackgroundAnr) {
814+
815+
// Skip background ANRs (as profiling only runs in foreground)
816+
if (isBackgroundAnr) {
817+
return;
818+
}
819+
820+
final String cacheDirPath = options.getCacheDirPath();
821+
if (cacheDirPath == null) {
822+
return;
823+
}
824+
final File cacheDir = new File(cacheDirPath);
825+
826+
if (!(hint instanceof AbnormalExit)) {
827+
return;
828+
}
829+
final Long anrTimestampObj = ((AbnormalExit) hint).timestamp();
830+
final long anrTimestamp;
831+
if (anrTimestampObj != null) {
832+
anrTimestamp = anrTimestampObj;
833+
} else if (event.getTimestamp() != null) {
834+
anrTimestamp = event.getTimestamp().getTime();
835+
} else {
836+
return;
837+
}
838+
839+
AnrProfile anrProfile = null;
840+
try {
841+
final File lastFile = AnrProfileRotationHelper.getLastFile(cacheDir);
842+
if (lastFile.exists()) {
843+
options.getLogger().log(SentryLevel.DEBUG, "Reading ANR profile");
844+
try (final AnrProfileManager provider = new AnrProfileManager(options, lastFile)) {
845+
anrProfile = provider.load();
846+
}
847+
} else {
848+
options.getLogger().log(SentryLevel.DEBUG, "No ANR profile file found");
849+
}
850+
} catch (Throwable t) {
851+
options.getLogger().log(SentryLevel.INFO, "Could not retrieve ANR profile", t);
852+
} finally {
853+
if (!AnrProfileRotationHelper.deleteLastFile(cacheDir)) {
854+
options.getLogger().log(SentryLevel.INFO, "Could not delete ANR profile file");
855+
}
856+
}
857+
858+
if (anrProfile == null) {
859+
return;
860+
}
861+
862+
options.getLogger().log(SentryLevel.INFO, "ANR profile found");
863+
if (anrTimestamp < anrProfile.startTimeMs || anrTimestamp > anrProfile.endTimeMs) {
864+
options.getLogger().log(SentryLevel.DEBUG, "ANR profile found, but doesn't match");
865+
return;
866+
}
867+
868+
final AggregatedStackTrace culprit = AnrCulpritIdentifier.identify(anrProfile.stacks);
869+
if (culprit == null) {
870+
return;
871+
}
872+
873+
// Capture profile chunk
874+
final @Nullable SentryId profilerId = captureAnrProfile(anrTimestamp, anrProfile);
875+
final @NotNull StackTraceElement[] stack = culprit.getStack();
876+
877+
if (stack.length > 0) {
878+
final StackTraceElement stackTraceElement = stack[0];
879+
final String message =
880+
stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName();
881+
final ApplicationNotResponding exception = new ApplicationNotResponding(message);
882+
exception.setStackTrace(stack);
883+
884+
final Mechanism mechanism = new Mechanism();
885+
mechanism.setType("ANR");
886+
final ExceptionMechanismException error =
887+
new ExceptionMechanismException(mechanism, exception, null, false);
888+
889+
final @NotNull List<SentryException> sentryException =
890+
sentryExceptionFactory.getSentryExceptions(error);
891+
892+
// Replace the original ANR exception with the profile-derived one,
893+
// as we assume the profiling culprit identification is more valuable
894+
// the event threads are kept as-is, so the original main thread stacktrace is still
895+
// available
896+
event.setExceptions(sentryException);
897+
898+
if (profilerId != null) {
899+
event.getContexts().setProfile(new ProfileContext(profilerId));
900+
}
901+
}
902+
}
903+
904+
@Nullable
905+
private SentryId captureAnrProfile(final long anrTimestampMs, @NotNull AnrProfile anrProfile) {
906+
final SentryProfile profile = StackTraceConverter.convert(anrProfile);
907+
final ProfileChunk chunk =
908+
new ProfileChunk(
909+
new SentryId(),
910+
new SentryId(),
911+
null,
912+
new HashMap<>(0),
913+
anrTimestampMs / 1000.0d,
914+
ProfileChunk.PLATFORM_JAVA,
915+
options);
916+
chunk.setSentryProfile(profile);
917+
918+
final SentryId profilerId = Sentry.getCurrentScopes().captureProfileChunk(chunk);
919+
if (SentryId.EMPTY_ID.equals(profilerId)) {
920+
return null;
921+
} else {
922+
return chunk.getProfilerId();
923+
}
924+
}
925+
926+
private boolean hasOnlySystemFrames(@NotNull SentryEvent event) {
927+
final List<SentryException> exceptions = event.getExceptions();
928+
if (exceptions == null || exceptions.isEmpty()) {
929+
return false;
930+
}
931+
932+
for (final SentryException exception : exceptions) {
933+
final SentryStackTrace stacktrace = exception.getStacktrace();
934+
if (stacktrace != null) {
935+
final List<SentryStackFrame> frames = stacktrace.getFrames();
936+
if (frames != null && !frames.isEmpty()) {
937+
for (final SentryStackFrame frame : frames) {
938+
if (frame.isInApp() != null && frame.isInApp()) {
939+
return false;
940+
}
941+
final String module = frame.getModule();
942+
if (module != null && !AnrCulpritIdentifier.isSystemFrame(module)) {
943+
return false;
944+
}
945+
}
946+
}
947+
}
948+
}
949+
return true;
950+
}
780951
}
781952
}

0 commit comments

Comments
 (0)