From 095337449a500026266ace2228312a97e3a417a7 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 22 Jun 2023 17:19:53 +0200 Subject: [PATCH 1/5] Track auto-init usage in manifest --- .../main/java/io/sentry/android/core/SentryInitProvider.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryInitProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryInitProvider.java index 9e0460f8122..6d88bdad631 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryInitProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryInitProvider.java @@ -4,6 +4,7 @@ import android.content.pm.ProviderInfo; import android.net.Uri; import io.sentry.Sentry; +import io.sentry.SentryIntegrationPackageStorage; import io.sentry.SentryLevel; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -22,6 +23,7 @@ public boolean onCreate() { } if (ManifestMetadataReader.isAutoInit(context, logger)) { SentryAndroid.init(context, logger); + SentryIntegrationPackageStorage.getInstance().addIntegration("AutoInit"); } return true; } From 289d62554464632da3b75a0737d5a820e32a56af Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 22 Jun 2023 13:37:34 +0200 Subject: [PATCH 2/5] Improve ANRv2 implementation based on customer feedback (#2792) --- CHANGELOG.md | 10 ++ .../api/sentry-android-core.api | 4 + sentry-android-core/proguard-rules.pro | 2 + .../android/core/AnrV2EventProcessor.java | 22 +++- .../sentry/android/core/AnrV2Integration.java | 119 ++++++++++++++++-- .../android/core/SentryAndroidOptions.java | 37 ++++++ .../internal/threaddump/ThreadDumpParser.java | 10 +- .../android/core/AnrV2EventProcessorTest.kt | 10 ++ .../android/core/AnrV2IntegrationTest.kt | 70 ++++++++++- .../sentry/android/core/SentryAndroidTest.kt | 38 +++++- .../threaddump/ThreadDumpParserTest.kt | 4 +- sentry/api/sentry.api | 3 + .../src/main/java/io/sentry/Attachment.java | 10 ++ sentry/src/main/java/io/sentry/Hint.java | 10 ++ .../src/main/java/io/sentry/SentryClient.java | 5 + .../java/io/sentry/cache/EnvelopeCache.java | 2 +- .../test/java/io/sentry/SentryClientTest.kt | 37 ++++++ .../src/test/java/io/sentry/hints/HintTest.kt | 11 ++ 18 files changed, 380 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bd46b6a4d8..ec8bf833fcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ ### Features - Add debouncing mechanism and before-capture callbacks for screenshots and view hierarchies ([#2773](https://github.com/getsentry/sentry-java/pull/2773)) +- Improve ANRv2 implementation ([#2792](https://github.com/getsentry/sentry-java/pull/2792)) + - Add a proguard rule to keep `ApplicationNotResponding` class from obfuscation + - Add a new option `setReportHistoricalAnrs`; when enabled, it will report all of the ANRs from the [getHistoricalExitReasons](https://developer.android.com/reference/android/app/ActivityManager?hl=en#getHistoricalProcessExitReasons(java.lang.String,%20int,%20int)) list. + By default, the SDK only reports and enriches the latest ANR and only this one counts towards ANR rate. + Worth noting that this option is mainly useful when updating the SDK to the version where ANRv2 has been introduced, to report all ANRs happened prior to the SDK update. After that, the SDK will always pick up the latest ANR from the historical exit reasons list on next app restart, so there should be no historical ANRs to report. + These ANRs are reported with the `HistoricalAppExitInfo` mechanism. + - Add a new option `setAttachAnrThreadDump` to send ANR thread dump from the system as an attachment. + This is only useful as additional information, because the SDK attempts to parse the thread dump into proper threads with stacktraces by default. + - If [ApplicationExitInfo#getTraceInputStream](https://developer.android.com/reference/android/app/ApplicationExitInfo#getTraceInputStream()) returns null, the SDK no longer reports an ANR event, as these events are not very useful without it. + - Enhance regex patterns for native stackframes ## 6.23.0 diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 7fd64ed8c53..ccfe1950016 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -216,6 +216,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun getStartupCrashDurationThresholdMillis ()J public fun isAnrEnabled ()Z public fun isAnrReportInDebug ()Z + public fun isAttachAnrThreadDump ()Z public fun isAttachScreenshot ()Z public fun isAttachViewHierarchy ()Z public fun isCollectAdditionalContext ()Z @@ -228,9 +229,11 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun isEnableNetworkEventBreadcrumbs ()Z public fun isEnableRootCheck ()Z public fun isEnableSystemEventBreadcrumbs ()Z + public fun isReportHistoricalAnrs ()Z public fun setAnrEnabled (Z)V public fun setAnrReportInDebug (Z)V public fun setAnrTimeoutIntervalMillis (J)V + public fun setAttachAnrThreadDump (Z)V public fun setAttachScreenshot (Z)V public fun setAttachViewHierarchy (Z)V public fun setBeforeScreenshotCaptureCallback (Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;)V @@ -249,6 +252,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setNativeSdkName (Ljava/lang/String;)V public fun setProfilingTracesHz (I)V public fun setProfilingTracesIntervalMillis (I)V + public fun setReportHistoricalAnrs (Z)V } public abstract interface class io/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback { diff --git a/sentry-android-core/proguard-rules.pro b/sentry-android-core/proguard-rules.pro index 6e3c84c3bba..79daaace033 100644 --- a/sentry-android-core/proguard-rules.pro +++ b/sentry-android-core/proguard-rules.pro @@ -37,6 +37,8 @@ # Also keep method names, as they're e.g. used by native via JNI calls -keep class * extends io.sentry.SentryOptions { *; } +-keepnames class io.sentry.android.core.ApplicationNotResponding + ##---------------End: proguard configuration for android-core ---------- ##---------------Begin: proguard configuration for sentry-apollo-3 ---------- diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java index 7f338af54ce..cdbbd12c6b5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java @@ -152,8 +152,12 @@ private void backfillScope(final @NotNull SentryEvent event) { private void setTrace(final @NotNull SentryEvent event) { final SpanContext spanContext = PersistingScopeObserver.read(options, TRACE_FILENAME, SpanContext.class); - if (event.getContexts().getTrace() == null && spanContext != null) { - event.getContexts().setTrace(spanContext); + if (event.getContexts().getTrace() == null) { + if (spanContext != null + && spanContext.getSpanId() != null + && spanContext.getTraceId() != null) { + event.getContexts().setTrace(spanContext); + } } } @@ -190,8 +194,13 @@ private void setContexts(final @NotNull SentryBaseEvent event) { } final Contexts eventContexts = event.getContexts(); for (Map.Entry entry : new Contexts(persistedContexts).entrySet()) { + final Object value = entry.getValue(); + if (SpanContext.TYPE.equals(entry.getKey()) && value instanceof SpanContext) { + // we fill it in setTrace later on + continue; + } if (!eventContexts.containsKey(entry.getKey())) { - eventContexts.put(entry.getKey(), entry.getValue()); + eventContexts.put(entry.getKey(), value); } } } @@ -446,7 +455,12 @@ private void setExceptions(final @NotNull SentryEvent event, final @NotNull Obje // AnrV2 threads contain a thread dump from the OS, so we just search for the main thread dump // and make an exception out of its stacktrace final Mechanism mechanism = new Mechanism(); - mechanism.setType("AppExitInfo"); + if (!((Backfillable) hint).shouldEnrich()) { + // we only enrich the latest ANR in the list, so this is historical + mechanism.setType("HistoricalAppExitInfo"); + } else { + mechanism.setType("AppExitInfo"); + } final boolean isBackgroundAnr = isBackgroundAnr(hint); String message = "ANR"; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index 17dbff486e5..f3d257d225b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -4,6 +4,7 @@ import android.app.ActivityManager; import android.app.ApplicationExitInfo; import android.content.Context; +import io.sentry.Attachment; import io.sentry.DateUtils; import io.sentry.Hint; import io.sentry.IHub; @@ -20,6 +21,7 @@ import io.sentry.hints.AbnormalExit; import io.sentry.hints.Backfillable; import io.sentry.hints.BlockingFlushHint; +import io.sentry.protocol.Message; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryThread; import io.sentry.transport.CurrentDateProvider; @@ -27,8 +29,11 @@ import io.sentry.util.HintUtils; import io.sentry.util.Objects; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collections; @@ -186,8 +191,10 @@ public void run() { return; } - // report the remainder without enriching - reportNonEnrichedHistoricalAnrs(exitInfos, lastReportedAnrTimestamp); + if (options.isReportHistoricalAnrs()) { + // report the remainder without enriching + reportNonEnrichedHistoricalAnrs(exitInfos, lastReportedAnrTimestamp); + } // report the latest ANR with enriching, if contexts are available, otherwise report it // non-enriched @@ -228,7 +235,16 @@ private void reportAsSentryEvent( final boolean isBackground = exitInfo.getImportance() != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; - final List threads = parseThreadDump(exitInfo, isBackground); + final ParseResult result = parseThreadDump(exitInfo, isBackground); + if (result.type == ParseResult.Type.NO_DUMP) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Not reporting ANR event as there was no thread dump for the ANR %s", + exitInfo.toString()); + return; + } final AnrV2Hint anrHint = new AnrV2Hint( options.getFlushTimeoutMillis(), @@ -240,9 +256,24 @@ private void reportAsSentryEvent( final Hint hint = HintUtils.createWithTypeCheckHint(anrHint); final SentryEvent event = new SentryEvent(); - event.setThreads(threads); - event.setTimestamp(DateUtils.getDateTime(anrTimestamp)); + if (result.type == ParseResult.Type.ERROR) { + final Message sentryMessage = new Message(); + sentryMessage.setFormatted( + "Sentry Android SDK failed to parse system thread dump for " + + "this ANR. We recommend enabling [SentryOptions.isAttachAnrThreadDump] option " + + "to attach the thread dump as plain text and report this issue on GitHub."); + event.setMessage(sentryMessage); + } else if (result.type == ParseResult.Type.DUMP) { + event.setThreads(result.threads); + } event.setLevel(SentryLevel.FATAL); + event.setTimestamp(DateUtils.getDateTime(anrTimestamp)); + + if (options.isAttachAnrThreadDump()) { + if (result.dump != null) { + hint.setThreadDump(Attachment.fromThreadDump(result.dump)); + } + } final @NotNull SentryId sentryId = hub.captureEvent(event, hint); final boolean isEventDropped = sentryId.equals(SentryId.EMPTY_ID); @@ -259,20 +290,56 @@ private void reportAsSentryEvent( } } - private @Nullable List parseThreadDump( + private @NotNull ParseResult parseThreadDump( final @NotNull ApplicationExitInfo exitInfo, final boolean isBackground) { - List threads = null; + InputStream trace; + try { + trace = exitInfo.getTraceInputStream(); + if (trace == null) { + return new ParseResult(ParseResult.Type.NO_DUMP); + } + } catch (Throwable e) { + options.getLogger().log(SentryLevel.WARNING, "Failed to read ANR thread dump", e); + return new ParseResult(ParseResult.Type.NO_DUMP); + } + + byte[] dump = null; + try { + dump = getDumpBytes(trace); + } catch (Throwable e) { + options + .getLogger() + .log(SentryLevel.WARNING, "Failed to convert ANR thread dump to byte array", e); + } + try (final BufferedReader reader = - new BufferedReader(new InputStreamReader(exitInfo.getTraceInputStream()))) { + new BufferedReader(new InputStreamReader(new ByteArrayInputStream(dump)))) { final Lines lines = Lines.readLines(reader); final ThreadDumpParser threadDumpParser = new ThreadDumpParser(options, isBackground); - threads = threadDumpParser.parse(lines); + final List threads = threadDumpParser.parse(lines); + if (threads.isEmpty()) { + // if the list is empty this means our regex matching is garbage and this is still error + return new ParseResult(ParseResult.Type.ERROR, dump); + } + return new ParseResult(ParseResult.Type.DUMP, dump, threads); } catch (Throwable e) { options.getLogger().log(SentryLevel.WARNING, "Failed to parse ANR thread dump", e); + return new ParseResult(ParseResult.Type.ERROR, dump); + } + } + + private byte[] getDumpBytes(final @NotNull InputStream trace) throws IOException { + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + int nRead; + final byte[] data = new byte[1024]; + + while ((nRead = trace.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); } - return threads; + return buffer.toByteArray(); } } @@ -313,4 +380,36 @@ public String mechanism() { return isBackgroundAnr ? "anr_background" : "anr_foreground"; } } + + static final class ParseResult { + + enum Type { + DUMP, + NO_DUMP, + ERROR + } + + final Type type; + final byte[] dump; + final @Nullable List threads; + + ParseResult(final @NotNull Type type) { + this.type = type; + this.dump = null; + this.threads = null; + } + + ParseResult(final @NotNull Type type, final byte[] dump) { + this.type = type; + this.dump = dump; + this.threads = null; + } + + ParseResult( + final @NotNull Type type, final byte[] dump, final @Nullable List threads) { + this.type = type; + this.dump = dump; + this.threads = threads; + } + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index f28112ca118..62437a0425f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import android.app.ActivityManager; +import android.app.ApplicationExitInfo; import io.sentry.Hint; import io.sentry.ISpan; import io.sentry.Scope; @@ -8,6 +10,7 @@ import io.sentry.SentryOptions; import io.sentry.SpanStatus; import io.sentry.android.core.internal.util.RootChecker; +import io.sentry.protocol.Mechanism; import io.sentry.protocol.SdkVersion; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -182,6 +185,24 @@ public interface BeforeCaptureCallback { boolean execute(@NotNull SentryEvent event, @NotNull Hint hint, boolean debounce); } + /** + * Controls whether to report historical ANRs (v2) from the {@link ApplicationExitInfo} system + * API. When enabled, reports all of the ANRs available in the {@link + * ActivityManager#getHistoricalProcessExitReasons(String, int, int)} list, as opposed to + * reporting only the latest one. + * + *

These events do not affect ANR rate nor are they enriched with additional information from + * {@link Scope} like breadcrumbs. The events are reported with 'HistoricalAppExitInfo' {@link + * Mechanism}. + */ + private boolean reportHistoricalAnrs = false; + + /** + * Controls whether to send ANR (v2) thread dump as an attachment with plain text. The thread dump + * is being attached from {@link ApplicationExitInfo#getTraceInputStream()}, if available. + */ + private boolean attachAnrThreadDump = false; + public SentryAndroidOptions() { setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME); setSdkVersion(createSdkVersion()); @@ -510,4 +531,20 @@ public void setBeforeViewHierarchyCaptureCallback( final @NotNull BeforeCaptureCallback beforeViewHierarchyCaptureCallback) { this.beforeViewHierarchyCaptureCallback = beforeViewHierarchyCaptureCallback; } + + public boolean isReportHistoricalAnrs() { + return reportHistoricalAnrs; + } + + public void setReportHistoricalAnrs(final boolean reportHistoricalAnrs) { + this.reportHistoricalAnrs = reportHistoricalAnrs; + } + + public boolean isAttachAnrThreadDump() { + return attachAnrThreadDump; + } + + public void setAttachAnrThreadDump(final boolean attachAnrThreadDump) { + this.attachAnrThreadDump = attachAnrThreadDump; + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java index 850d8da2111..da90653092e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java @@ -39,9 +39,11 @@ public class ThreadDumpParser { private static final Pattern BEGIN_MANAGED_THREAD_RE = Pattern.compile("\"(.*)\" (.*) ?prio=(\\d+)\\s+tid=(\\d+)\\s*(.*)"); private static final Pattern NATIVE_RE = - Pattern.compile(" (?:native: )?#\\d+ \\S+ [0-9a-fA-F]+\\s+(.*)\\s+\\((.*)\\+(\\d+)\\)"); + Pattern.compile( + " (?:native: )?#\\d+ \\S+ [0-9a-fA-F]+\\s+(.*?)\\s+\\((.*)\\+(\\d+)\\)(?: \\(.*\\))?"); private static final Pattern NATIVE_NO_LOC_RE = - Pattern.compile(" (?:native: )?#\\d+ \\S+ [0-9a-fA-F]+\\s+(.*)\\s*\\(?(.*)\\)?"); + Pattern.compile( + " (?:native: )?#\\d+ \\S+ [0-9a-fA-F]+\\s+(.*)\\s*\\(?(.*)\\)?(?: \\(.*\\))?"); private static final Pattern JAVA_RE = Pattern.compile(" at (?:(.+)\\.)?([^.]+)\\.([^.]+)\\((.*):([\\d-]+)\\)"); private static final Pattern JNI_RE = @@ -179,14 +181,14 @@ private SentryStackTrace parseStacktrace( if (matches(nativeRe, text)) { final SentryStackFrame frame = new SentryStackFrame(); frame.setPackage(nativeRe.group(1)); - frame.setSymbol(nativeRe.group(2)); + frame.setFunction(nativeRe.group(2)); frame.setLineno(getInteger(nativeRe, 3, null)); frames.add(frame); lastJavaFrame = null; } else if (matches(nativeNoLocRe, text)) { final SentryStackFrame frame = new SentryStackFrame(); frame.setPackage(nativeNoLocRe.group(1)); - frame.setSymbol(nativeNoLocRe.group(2)); + frame.setFunction(nativeNoLocRe.group(2)); frames.add(frame); lastJavaFrame = null; } else if (matches(javaRe, text)) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt index a52aee527a6..4b0e6de7b1e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt @@ -107,6 +107,7 @@ class AnrV2EventProcessorTest { persistScope( CONTEXTS_FILENAME, Contexts().apply { + trace = SpanContext("test") setResponse(Response().apply { bodySize = 1024 }) setBrowser(Browser().apply { name = "Google Chrome" }) } @@ -169,6 +170,15 @@ class AnrV2EventProcessorTest { assertEquals(emptyMap(), processed.contexts) } + @Test + fun `when backfillable event is not enrichable, sets different mechanism`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint(shouldEnrich = false)) + + val processed = processEvent(hint) + + assertEquals("HistoricalAppExitInfo", processed.exceptions!![0].mechanism!!.type) + } + @Test fun `when backfillable event is not enrichable, sets platform`() { val hint = HintUtils.createWithTypeCheckHint(BackfillableHint(shouldEnrich = false)) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt index 4db0d425c47..7fce1a126ea 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt @@ -24,6 +24,7 @@ import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat +import org.mockito.kotlin.atMost import org.mockito.kotlin.check import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock @@ -42,6 +43,7 @@ import kotlin.concurrent.thread import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @@ -67,7 +69,9 @@ class AnrV2IntegrationTest { flushTimeoutMillis: Long = 0L, lastReportedAnrTimestamp: Long? = null, lastEventId: SentryId = SentryId(), - sessionTrackingEnabled: Boolean = true + sessionTrackingEnabled: Boolean = true, + reportHistoricalAnrs: Boolean = true, + attachAnrThreadDump: Boolean = false ): AnrV2Integration { options.run { setLogger(this@Fixture.logger) @@ -78,6 +82,8 @@ class AnrV2IntegrationTest { this.isAnrEnabled = isAnrEnabled this.flushTimeoutMillis = flushTimeoutMillis this.isEnableAutoSessionTracking = sessionTrackingEnabled + this.isReportHistoricalAnrs = reportHistoricalAnrs + this.isAttachAnrThreadDump = attachAnrThreadDump addInAppInclude("io.sentry.samples") setEnvelopeDiskCache(EnvelopeCache.create(this)) } @@ -93,7 +99,8 @@ class AnrV2IntegrationTest { fun addAppExitInfo( reason: Int? = ApplicationExitInfo.REASON_ANR, timestamp: Long? = null, - importance: Int? = null + importance: Int? = null, + addTrace: Boolean = true ) { val builder = ApplicationExitInfoBuilder.newBuilder() if (reason != null) { @@ -106,6 +113,9 @@ class AnrV2IntegrationTest { builder.setImportance(importance) } val exitInfo = spy(builder.build()) { + if (!addTrace) { + return + } whenever(mock.traceInputStream).thenReturn( """ "main" prio=5 tid=1 Blocked @@ -259,9 +269,11 @@ class AnrV2IntegrationTest { assertEquals(false, otherThread.isMain) val firstFrame = otherThread.stacktrace!!.frames!!.first() assertEquals( - "/apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b)", + "/apex/com.android.runtime/lib64/bionic/libc.so", firstFrame.`package` ) + assertEquals("__start_thread", firstFrame.function) + assertEquals(64, firstFrame.lineno) }, argThat { val hint = HintUtils.getSentrySdkHint(this) @@ -358,6 +370,29 @@ class AnrV2IntegrationTest { ) } + @Test + fun `when historical ANRs flag is disabled, does not report`() { + val integration = fixture.getSut( + tmpDir, + lastReportedAnrTimestamp = oldTimestamp, + reportHistoricalAnrs = false + ) + fixture.addAppExitInfo(timestamp = newTimestamp - 2 * 60 * 1000) + fixture.addAppExitInfo(timestamp = newTimestamp - 1 * 60 * 1000) + fixture.addAppExitInfo(timestamp = newTimestamp) + + integration.register(fixture.hub, fixture.options) + + // only the latest anr is reported which should be enrichable + verify(fixture.hub, atMost(1)).captureEvent( + any(), + argThat { + val hint = HintUtils.getSentrySdkHint(this) + (hint as AnrV2Hint).shouldEnrich() + } + ) + } + @Test fun `historical ANRs are reported in reverse order to keep track of last reported ANR in a marker file`() { val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) @@ -486,4 +521,33 @@ class AnrV2IntegrationTest { // should return true, because latch is 0 now assertTrue((fixture.options.envelopeDiskCache as EnvelopeCache).waitPreviousSessionFlush()) } + + @Test + fun `attaches plain thread dump, if enabled`() { + val integration = fixture.getSut( + tmpDir, + lastReportedAnrTimestamp = oldTimestamp, + attachAnrThreadDump = true + ) + fixture.addAppExitInfo(timestamp = newTimestamp) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub).captureEvent( + any(), + check { + assertNotNull(it.threadDump) + } + ) + } + + @Test + fun `when traceInputStream is null, does not report ANR`() { + val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) + fixture.addAppExitInfo(timestamp = newTimestamp, addTrace = false) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index 5ec657c21ca..876bb3bd786 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -41,7 +41,9 @@ import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.spy import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.robolectric.annotation.Config import org.robolectric.shadow.api.Shadow import org.robolectric.shadows.ShadowActivityManager @@ -101,7 +103,41 @@ class SentryAndroidTest { if (importance != null) { builder.setImportance(importance) } - shadowActivityManager.addApplicationExitInfo(builder.build()) + val exitInfo = spy(builder.build()) { + whenever(mock.traceInputStream).thenReturn( + """ +"main" prio=5 tid=1 Blocked + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x72a985e0 self=0xb400007cabc57380 + | sysTid=28941 nice=-10 cgrp=top-app sched=0/0 handle=0x7deceb74f8 + | state=S schedstat=( 324804784 183300334 997 ) utm=23 stm=8 core=3 HZ=100 + | stack=0x7ff93a9000-0x7ff93ab000 stackSize=8188KB + | held mutexes= + at io.sentry.samples.android.MainActivity${'$'}2.run(MainActivity.java:177) + - waiting to lock <0x0d3a2f0a> (a java.lang.Object) held by thread 5 + at android.os.Handler.handleCallback(Handler.java:942) + at android.os.Handler.dispatchMessage(Handler.java:99) + at android.os.Looper.loopOnce(Looper.java:201) + at android.os.Looper.loop(Looper.java:288) + at android.app.ActivityThread.main(ActivityThread.java:7872) + at java.lang.reflect.Method.invoke(Native method) + at com.android.internal.os.RuntimeInit${'$'}MethodAndArgsCaller.run(RuntimeInit.java:548) + at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936) + +"perfetto_hprof_listener" prio=10 tid=7 Native (still starting up) + | group="" sCount=1 ucsCount=0 flags=1 obj=0x0 self=0xb400007cabc5ab20 + | sysTid=28959 nice=-20 cgrp=top-app sched=0/0 handle=0x7b2021bcb0 + | state=S schedstat=( 72750 1679167 1 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x7b20124000-0x7b20126000 stackSize=991KB + | held mutexes= + native: #00 pc 00000000000a20f4 /apex/com.android.runtime/lib64/bionic/libc.so (read+4) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 000000000001d840 /apex/com.android.art/lib64/libperfetto_hprof.so (void* std::__1::__thread_proxy >, ArtPlugin_Initialize::${'$'}_34> >(void*)+260) (BuildId: 525cc92a7dc49130157aeb74f6870364) + native: #02 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #03 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + (no managed stack frames) + """.trimIndent().byteInputStream() + ) + } + shadowActivityManager.addApplicationExitInfo(exitInfo) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt index 3f91a051f48..c2996c5d088 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt @@ -60,9 +60,11 @@ class ThreadDumpParserTest { assertEquals(false, randomThread.isMain) assertEquals(false, randomThread.isCurrent) assertEquals( - "/apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b)", + "/apex/com.android.runtime/lib64/bionic/libc.so", randomThread.stacktrace!!.frames!!.last().`package` ) + assertEquals("__epoll_pwait", randomThread.stacktrace!!.frames!!.last()!!.function) + assertEquals(8, randomThread.stacktrace!!.frames!!.last()!!.lineno) val firstFrame = randomThread.stacktrace!!.frames!!.first() assertEquals("android.os.HandlerThread", firstFrame.module) assertEquals("run", firstFrame.function) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 187725650ec..0e9d4c1a848 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -16,6 +16,7 @@ public final class io/sentry/Attachment { public fun ([BLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V public fun ([BLjava/lang/String;Ljava/lang/String;Z)V public static fun fromScreenshot ([B)Lio/sentry/Attachment; + public static fun fromThreadDump ([B)Lio/sentry/Attachment; public static fun fromViewHierarchy (Lio/sentry/protocol/ViewHierarchy;)Lio/sentry/Attachment; public fun getAttachmentType ()Ljava/lang/String; public fun getBytes ()[B @@ -309,11 +310,13 @@ public final class io/sentry/Hint { public fun getAs (Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object; public fun getAttachments ()Ljava/util/List; public fun getScreenshot ()Lio/sentry/Attachment; + public fun getThreadDump ()Lio/sentry/Attachment; public fun getViewHierarchy ()Lio/sentry/Attachment; public fun remove (Ljava/lang/String;)V public fun replaceAttachments (Ljava/util/List;)V public fun set (Ljava/lang/String;Ljava/lang/Object;)V public fun setScreenshot (Lio/sentry/Attachment;)V + public fun setThreadDump (Lio/sentry/Attachment;)V public fun setViewHierarchy (Lio/sentry/Attachment;)V public static fun withAttachment (Lio/sentry/Attachment;)Lio/sentry/Hint; public static fun withAttachments (Ljava/util/List;)Lio/sentry/Hint; diff --git a/sentry/src/main/java/io/sentry/Attachment.java b/sentry/src/main/java/io/sentry/Attachment.java index 4f2e12fe409..7a4ec3b99dc 100644 --- a/sentry/src/main/java/io/sentry/Attachment.java +++ b/sentry/src/main/java/io/sentry/Attachment.java @@ -334,4 +334,14 @@ boolean isAddToTransactions() { VIEW_HIERARCHY_ATTACHMENT_TYPE, false); } + + /** + * Creates a new Thread Dump Attachment + * + * @param bytes the array bytes + * @return the Attachment + */ + public static @NotNull Attachment fromThreadDump(final byte[] bytes) { + return new Attachment(bytes, "thread-dump.txt", "text/plain", false); + } } diff --git a/sentry/src/main/java/io/sentry/Hint.java b/sentry/src/main/java/io/sentry/Hint.java index ab945c56055..07dde3cb807 100644 --- a/sentry/src/main/java/io/sentry/Hint.java +++ b/sentry/src/main/java/io/sentry/Hint.java @@ -30,6 +30,8 @@ public final class Hint { private @Nullable Attachment screenshot = null; private @Nullable Attachment viewHierarchy = null; + private @Nullable Attachment threadDump = null; + public static @NotNull Hint withAttachment(@Nullable Attachment attachment) { @NotNull final Hint hint = new Hint(); hint.addAttachment(attachment); @@ -126,6 +128,14 @@ public void setViewHierarchy(final @Nullable Attachment viewHierarchy) { return viewHierarchy; } + public void setThreadDump(final @Nullable Attachment threadDump) { + this.threadDump = threadDump; + } + + public @Nullable Attachment getThreadDump() { + return threadDump; + } + private boolean isCastablePrimitive(@Nullable Object hintValue, @NotNull Class clazz) { Class nonPrimitiveClass = PRIMITIVE_MAPPINGS.get(clazz.getCanonicalName()); return hintValue != null diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 7570611b58e..77f3e1462eb 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -251,6 +251,11 @@ private boolean shouldSendSessionUpdateForDroppedEvent( attachments.add(viewHierarchy); } + @Nullable final Attachment threadDump = hint.getThreadDump(); + if (threadDump != null) { + attachments.add(threadDump); + } + return attachments; } diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index f5c531628c8..24d7c62aa3d 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -154,7 +154,7 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi SentryCrashLastRunState.getInstance().setCrashedLastRun(crashedLastRun); - previousSessionLatch.countDown(); + flushPreviousSession(); } // TODO: probably we need to update the current session file for session updates to because of diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 19dff0a5040..b17235f7385 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -1537,6 +1537,42 @@ class SentryClientTest { ) } + @Test + fun `thread dump is added to the envelope from the hint`() { + val sut = fixture.getSut() + val attachment = Attachment.fromThreadDump(byteArrayOf()) + val hint = Hint().also { it.threadDump = attachment } + + sut.captureEvent(SentryEvent(), hint) + + verify(fixture.transport).send( + check { envelope -> + val threadDump = envelope.items.last() + assertNotNull(threadDump) { + assertEquals(attachment.filename, threadDump.header.fileName) + } + }, + anyOrNull() + ) + } + + @Test + fun `thread dump is dropped from hint via before send`() { + fixture.sentryOptions.beforeSend = CustomBeforeSendCallback() + val sut = fixture.getSut() + val attachment = Attachment.fromThreadDump(byteArrayOf()) + val hint = Hint().also { it.threadDump = attachment } + + sut.captureEvent(SentryEvent(), hint) + + verify(fixture.transport).send( + check { envelope -> + assertEquals(1, envelope.items.count()) + }, + anyOrNull() + ) + } + @Test fun `capturing an error updates session and sends event + session`() { val sut = fixture.getSut() @@ -2127,6 +2163,7 @@ class SentryClientTest { override fun execute(event: SentryEvent, hint: Hint): SentryEvent? { hint.screenshot = null hint.viewHierarchy = null + hint.threadDump = null return event } } diff --git a/sentry/src/test/java/io/sentry/hints/HintTest.kt b/sentry/src/test/java/io/sentry/hints/HintTest.kt index 054baedcaef..520e0a723bb 100644 --- a/sentry/src/test/java/io/sentry/hints/HintTest.kt +++ b/sentry/src/test/java/io/sentry/hints/HintTest.kt @@ -209,6 +209,7 @@ class HintTest { hint.addAttachment(newAttachment("test attachment")) hint.screenshot = newAttachment("2") hint.viewHierarchy = newAttachment("3") + hint.threadDump = newAttachment("4") hint.clear() @@ -217,6 +218,7 @@ class HintTest { assertEquals(1, hint.attachments.size) assertNotNull(hint.screenshot) assertNotNull(hint.viewHierarchy) + assertNotNull(hint.threadDump) } @Test @@ -237,6 +239,15 @@ class HintTest { assertNotNull(hint.viewHierarchy) } + @Test + fun `can create hint with a thread dump`() { + val hint = Hint() + val attachment = newAttachment("thread-dump") + hint.threadDump = attachment + + assertNotNull(hint.threadDump) + } + companion object { fun newAttachment(content: String) = Attachment(content.toByteArray(), "$content.txt") } From 40c6eb0ffac0297714541f40216df117d895fee5 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 22 Jun 2023 12:22:48 +0000 Subject: [PATCH 3/5] release: 6.24.0 --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec8bf833fcd..abbceea8e6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 6.24.0 ### Features diff --git a/gradle.properties b/gradle.properties index cc8289cd434..f77afc13027 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ android.useAndroidX=true android.defaults.buildfeatures.buildconfig=true # Release information -versionName=6.23.0 +versionName=6.24.0 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android From 347fe0a97d81cc0e2db203287af7123f7dd6d3ce Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 22 Jun 2023 17:24:21 +0200 Subject: [PATCH 4/5] Changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index abbceea8e6c..048890638c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add manifest `AutoInit` to integrations list ([#2795](https://github.com/getsentry/sentry-java/pull/2795)) + ## 6.24.0 ### Features From b3e23b18612378d21c3f3b788784be9890443ae1 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 22 Jun 2023 17:25:23 +0200 Subject: [PATCH 5/5] Fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b28dabe55f..048890638c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 6.24.0 +## Unreleased ### Features