Skip to content

Commit 31581b9

Browse files
authored
Merge 0cd877d into 0eaac1e
2 parents 0eaac1e + 0cd877d commit 31581b9

30 files changed

Lines changed: 2311 additions & 38 deletions

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,13 @@
6868
- Discard envelopes on `4xx` and `5xx` response ([#4950](https://github.com/getsentry/sentry-java/pull/4950))
6969
- This aims to not overwhelm Sentry after an outage or load shedding (including HTTP 429) where too many events are sent at once
7070

71-
### Feature
71+
### Features
7272

73+
- Add new experimental option to capture profiles for ANRs ([#4899](https://github.com/getsentry/sentry-java/pull/4899))
74+
- This feature will capture a stack profile of the main thread when it gets unresponsive
75+
- 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
76+
- 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
77+
- Enable via `options.setEnableAnrProfiling(true)` or Android manifest: `<meta-data android:name="io.sentry.anr.enable-profiling" android:value="true" />`
7378
- 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))
7479
- Currently exposed via options as an _internal_ API only.
7580
- If enabled alongside the NDK integration, crashes will be reported as two separate events. Users should enable only one; deduplication between both integrations will be added in a future release.

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
351351
public fun isCollectExternalStorageContext ()Z
352352
public fun isEnableActivityLifecycleBreadcrumbs ()Z
353353
public fun isEnableActivityLifecycleTracingAutoFinish ()Z
354+
public fun isEnableAnrProfiling ()Z
354355
public fun isEnableAppComponentBreadcrumbs ()Z
355356
public fun isEnableAppLifecycleBreadcrumbs ()Z
356357
public fun isEnableAutoActivityLifecycleTracing ()Z
@@ -379,6 +380,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
379380
public fun setDebugImagesLoader (Lio/sentry/android/core/IDebugImagesLoader;)V
380381
public fun setEnableActivityLifecycleBreadcrumbs (Z)V
381382
public fun setEnableActivityLifecycleTracingAutoFinish (Z)V
383+
public fun setEnableAnrProfiling (Z)V
382384
public fun setEnableAppComponentBreadcrumbs (Z)V
383385
public fun setEnableAppLifecycleBreadcrumbs (Z)V
384386
public fun setEnableAutoActivityLifecycleTracing (Z)V
@@ -533,6 +535,84 @@ public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentr
533535
public static fun snapshotViewHierarchyAsData (Landroid/app/Activity;Lio/sentry/util/thread/IThreadChecker;Lio/sentry/ISerializer;Lio/sentry/ILogger;)[B
534536
}
535537

538+
public class io/sentry/android/core/anr/AggregatedStackTrace {
539+
public fun <init> ([Ljava/lang/StackTraceElement;IIJF)V
540+
public fun addOccurrence (J)V
541+
public fun getStack ()[Ljava/lang/StackTraceElement;
542+
}
543+
544+
public class io/sentry/android/core/anr/AnrCulpritIdentifier {
545+
public fun <init> ()V
546+
public static fun identify (Ljava/util/List;)Lio/sentry/android/core/anr/AggregatedStackTrace;
547+
public static fun isSystemFrame (Ljava/lang/String;)Z
548+
}
549+
550+
public class io/sentry/android/core/anr/AnrException : java/lang/Exception {
551+
public fun <init> ()V
552+
public fun <init> (Ljava/lang/String;)V
553+
}
554+
555+
public class io/sentry/android/core/anr/AnrProfile {
556+
public final field endtimeMs J
557+
public final field stacks Ljava/util/List;
558+
public final field startTimeMs J
559+
public fun <init> (Ljava/util/List;)V
560+
}
561+
562+
public class io/sentry/android/core/anr/AnrProfileManager : java/io/Closeable {
563+
public fun <init> (Lio/sentry/SentryOptions;)V
564+
public fun <init> (Lio/sentry/SentryOptions;Ljava/io/File;)V
565+
public fun add (Lio/sentry/android/core/anr/AnrStackTrace;)V
566+
public fun clear ()V
567+
public fun close ()V
568+
public fun load ()Lio/sentry/android/core/anr/AnrProfile;
569+
}
570+
571+
public class io/sentry/android/core/anr/AnrProfileRotationHelper {
572+
public fun <init> ()V
573+
public static fun deleteLastFile (Ljava/io/File;)Z
574+
public static fun getFileForRecording (Ljava/io/File;)Ljava/io/File;
575+
public static fun getLastFile (Ljava/io/File;)Ljava/io/File;
576+
public static fun rotate ()V
577+
}
578+
579+
public class io/sentry/android/core/anr/AnrProfilingIntegration : io/sentry/Integration, io/sentry/android/core/AppState$AppStateListener, java/io/Closeable, java/lang/Runnable {
580+
public static final field POLLING_INTERVAL_MS J
581+
public static final field THRESHOLD_ANR_MS J
582+
public fun <init> ()V
583+
protected fun checkMainThread (Ljava/lang/Thread;)V
584+
public fun close ()V
585+
protected fun getProfileManager ()Lio/sentry/android/core/anr/AnrProfileManager;
586+
protected fun getState ()Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
587+
public fun onBackground ()V
588+
public fun onForeground ()V
589+
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
590+
public fun run ()V
591+
}
592+
593+
protected final class io/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState : java/lang/Enum {
594+
public static final field ANR_DETECTED Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
595+
public static final field IDLE Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
596+
public static final field SUSPICIOUS Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
597+
public static fun valueOf (Ljava/lang/String;)Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
598+
public static fun values ()[Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
599+
}
600+
601+
public final class io/sentry/android/core/anr/AnrStackTrace : java/lang/Comparable {
602+
public final field stack [Ljava/lang/StackTraceElement;
603+
public final field timestampMs J
604+
public fun <init> (J[Ljava/lang/StackTraceElement;)V
605+
public fun compareTo (Lio/sentry/android/core/anr/AnrStackTrace;)I
606+
public synthetic fun compareTo (Ljava/lang/Object;)I
607+
public static fun deserialize (Ljava/io/DataInputStream;)Lio/sentry/android/core/anr/AnrStackTrace;
608+
public fun serialize (Ljava/io/DataOutputStream;)V
609+
}
610+
611+
public final class io/sentry/android/core/anr/StackTraceConverter {
612+
public fun <init> ()V
613+
public static fun convert (Lio/sentry/android/core/anr/AnrProfile;)Lio/sentry/protocol/profiling/SentryProfile;
614+
}
615+
536616
public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry/cache/EnvelopeCache {
537617
public static final field LAST_ANR_MARKER_LABEL Ljava/lang/String;
538618
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
}
@@ -399,6 +403,8 @@ static void installDefaultIntegrations(
399403
// it to set the replayId in case of an ANR
400404
options.addIntegration(AnrIntegrationFactory.create(context, buildInfoProvider));
401405

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

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

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,20 @@
1212
import io.sentry.ILogger;
1313
import io.sentry.IScopes;
1414
import io.sentry.Integration;
15+
import io.sentry.ProfileChunk;
16+
import io.sentry.ProfileContext;
1517
import io.sentry.SentryEvent;
18+
import io.sentry.SentryExceptionFactory;
1619
import io.sentry.SentryLevel;
1720
import io.sentry.SentryOptions;
21+
import io.sentry.SentryStackTraceFactory;
22+
import io.sentry.android.core.anr.AggregatedStackTrace;
23+
import io.sentry.android.core.anr.AnrCulpritIdentifier;
24+
import io.sentry.android.core.anr.AnrException;
25+
import io.sentry.android.core.anr.AnrProfile;
26+
import io.sentry.android.core.anr.AnrProfileManager;
27+
import io.sentry.android.core.anr.AnrProfileRotationHelper;
28+
import io.sentry.android.core.anr.StackTraceConverter;
1829
import io.sentry.android.core.cache.AndroidEnvelopeCache;
1930
import io.sentry.android.core.internal.threaddump.Lines;
2031
import io.sentry.android.core.internal.threaddump.ThreadDumpParser;
@@ -24,8 +35,12 @@
2435
import io.sentry.protocol.DebugImage;
2536
import io.sentry.protocol.DebugMeta;
2637
import io.sentry.protocol.Message;
38+
import io.sentry.protocol.SentryException;
2739
import io.sentry.protocol.SentryId;
40+
import io.sentry.protocol.SentryStackFrame;
41+
import io.sentry.protocol.SentryStackTrace;
2842
import io.sentry.protocol.SentryThread;
43+
import io.sentry.protocol.profiling.SentryProfile;
2944
import io.sentry.transport.CurrentDateProvider;
3045
import io.sentry.transport.ICurrentDateProvider;
3146
import io.sentry.util.HintUtils;
@@ -34,9 +49,12 @@
3449
import java.io.ByteArrayInputStream;
3550
import java.io.ByteArrayOutputStream;
3651
import java.io.Closeable;
52+
import java.io.File;
3753
import java.io.IOException;
3854
import java.io.InputStream;
3955
import java.io.InputStreamReader;
56+
import java.util.Arrays;
57+
import java.util.HashMap;
4058
import java.util.List;
4159
import org.jetbrains.annotations.ApiStatus;
4260
import org.jetbrains.annotations.NotNull;
@@ -86,7 +104,11 @@ public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) {
86104
.getExecutorService()
87105
.submit(
88106
new ApplicationExitInfoHistoryDispatcher(
89-
context, scopes, this.options, dateProvider, new AnrV2Policy(this.options)));
107+
context,
108+
scopes,
109+
this.options,
110+
dateProvider,
111+
new AnrV2Policy(scopes, this.options)));
90112
} catch (Throwable e) {
91113
options.getLogger().log(SentryLevel.DEBUG, "Failed to start ANR processor.", e);
92114
}
@@ -105,9 +127,11 @@ public void close() throws IOException {
105127
private static final class AnrV2Policy
106128
implements ApplicationExitInfoHistoryDispatcher.ApplicationExitInfoPolicy {
107129

130+
private final @NotNull IScopes scopes;
108131
private final @NotNull SentryAndroidOptions options;
109132

110-
AnrV2Policy(final @NotNull SentryAndroidOptions options) {
133+
AnrV2Policy(final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options) {
134+
this.scopes = scopes;
111135
this.options = options;
112136
}
113137

@@ -183,9 +207,111 @@ public boolean shouldReportHistorical() {
183207
}
184208
}
185209

210+
if (options.isEnableAnrProfiling()) {
211+
applyAnrProfile(isBackground, anrTimestamp, event);
212+
// TODO: maybe move to AnrV2EventProcessor instead
213+
if (hasOnlySystemFrames(event)) {
214+
// By omitting the {{ default }} fingerprint, the stacktrace will be completely ignored
215+
// and all events will be grouped
216+
// into the same issue
217+
event.setFingerprints(
218+
Arrays.asList(
219+
"{{ system-frames-only-anr }}",
220+
isBackground ? "background-anr" : "foreground-anr"));
221+
}
222+
}
223+
186224
return new ApplicationExitInfoHistoryDispatcher.Report(event, hint, anrHint);
187225
}
188226

227+
private void applyAnrProfile(
228+
final boolean isBackground, final long anrTimestamp, final @NotNull SentryEvent event) {
229+
230+
// as of now AnrProfilingIntegration only generates profiles in foreground
231+
if (isBackground) {
232+
return;
233+
}
234+
235+
final @Nullable String cacheDirPath = options.getCacheDirPath();
236+
if (cacheDirPath == null) {
237+
return;
238+
}
239+
final @NotNull File cacheDir = new File(cacheDirPath);
240+
241+
@Nullable AnrProfile anrProfile = null;
242+
243+
try {
244+
final File lastFile = AnrProfileRotationHelper.getLastFile(cacheDir);
245+
246+
if (lastFile.exists()) {
247+
options.getLogger().log(SentryLevel.DEBUG, "Reading ANR profile");
248+
try (final AnrProfileManager provider = new AnrProfileManager(options, lastFile)) {
249+
anrProfile = provider.load();
250+
}
251+
} else {
252+
options.getLogger().log(SentryLevel.DEBUG, "No ANR profile file found");
253+
}
254+
} catch (Throwable t) {
255+
options.getLogger().log(SentryLevel.INFO, "Could not retrieve ANR profile", t);
256+
} finally {
257+
if (!AnrProfileRotationHelper.deleteLastFile(cacheDir)) {
258+
options.getLogger().log(SentryLevel.INFO, "Could not delete ANR profile file");
259+
}
260+
}
261+
262+
if (anrProfile != null) {
263+
options.getLogger().log(SentryLevel.INFO, "ANR profile found");
264+
if (anrTimestamp >= anrProfile.startTimeMs && anrTimestamp <= anrProfile.endtimeMs) {
265+
final @Nullable AggregatedStackTrace culprit =
266+
AnrCulpritIdentifier.identify(anrProfile.stacks);
267+
if (culprit != null) {
268+
final @Nullable SentryId profilerId = captureAnrProfile(anrTimestamp, anrProfile);
269+
270+
final @NotNull StackTraceElement[] stack = culprit.getStack();
271+
if (stack.length > 0) {
272+
final StackTraceElement stackTraceElement = culprit.getStack()[0];
273+
final String message =
274+
stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName();
275+
final AnrException exception = new AnrException(message);
276+
exception.setStackTrace(stack);
277+
278+
// TODO should this be re-used from somewhere else?
279+
final SentryExceptionFactory factory =
280+
new SentryExceptionFactory(new SentryStackTraceFactory(options));
281+
event.setExceptions(factory.getSentryExceptions(exception));
282+
if (profilerId != null) {
283+
event.getContexts().setProfile(new ProfileContext(profilerId));
284+
}
285+
}
286+
}
287+
} else {
288+
options.getLogger().log(SentryLevel.DEBUG, "ANR profile found, but doesn't match");
289+
}
290+
}
291+
}
292+
293+
@Nullable
294+
private SentryId captureAnrProfile(
295+
final long anrTimestamp, final @NotNull AnrProfile anrProfile) {
296+
final SentryProfile profile = StackTraceConverter.convert(anrProfile);
297+
final ProfileChunk chunk =
298+
new ProfileChunk(
299+
new SentryId(),
300+
new SentryId(),
301+
null,
302+
new HashMap<>(0),
303+
anrTimestamp / 1000.0d,
304+
ProfileChunk.PLATFORM_JAVA,
305+
options);
306+
chunk.setSentryProfile(profile);
307+
final SentryId profilerId = scopes.captureProfileChunk(chunk);
308+
if (profilerId.equals(SentryId.EMPTY_ID)) {
309+
return null;
310+
} else {
311+
return chunk.getProfilerId();
312+
}
313+
}
314+
189315
private @NotNull ParseResult parseThreadDump(
190316
final @NotNull ApplicationExitInfo exitInfo, final boolean isBackground) {
191317
final byte[] dump;
@@ -239,6 +365,33 @@ private byte[] getDumpBytes(final @NotNull InputStream trace) throws IOException
239365
}
240366
}
241367

368+
private static boolean hasOnlySystemFrames(final @NotNull SentryEvent event) {
369+
final List<SentryException> exceptions = event.getExceptions();
370+
if (exceptions == null || exceptions.isEmpty()) {
371+
// No exceptions means we haven't verified frames - don't apply special fingerprinting
372+
return false;
373+
}
374+
375+
for (final SentryException exception : exceptions) {
376+
final @Nullable SentryStackTrace stacktrace = exception.getStacktrace();
377+
if (stacktrace != null) {
378+
final @Nullable List<SentryStackFrame> frames = stacktrace.getFrames();
379+
if (frames != null && !frames.isEmpty()) {
380+
for (final SentryStackFrame frame : frames) {
381+
if (frame.isInApp() != null && frame.isInApp()) {
382+
return false;
383+
}
384+
final @Nullable String module = frame.getModule();
385+
if (module != null && !AnrCulpritIdentifier.isSystemFrame(frame.getModule())) {
386+
return false;
387+
}
388+
}
389+
}
390+
}
391+
}
392+
return true;
393+
}
394+
242395
@ApiStatus.Internal
243396
public static final class AnrV2Hint extends BlockingFlushHint
244397
implements Backfillable, AbnormalExit {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ final class ManifestMetadataReader {
168168

169169
static final String SPOTLIGHT_CONNECTION_URL = "io.sentry.spotlight.url";
170170

171+
static final String ENABLE_ANR_PROFILING = "io.sentry.anr.enable-profiling";
172+
171173
/** ManifestMetadataReader ctor */
172174
private ManifestMetadataReader() {}
173175

@@ -655,6 +657,9 @@ static void applyMetadata(
655657
if (spotlightUrl != null) {
656658
options.setSpotlightConnectionUrl(spotlightUrl);
657659
}
660+
661+
options.setEnableAnrProfiling(
662+
readBool(metadata, logger, ENABLE_ANR_PROFILING, options.isEnableAnrProfiling()));
658663
}
659664
options
660665
.getLogger()

0 commit comments

Comments
 (0)