diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5632b6926b9..7faa69032a9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
### Features
+- Add option to attach raw tombstone protobuf on native crash events ([#5446](https://github.com/getsentry/sentry-java/pull/5446))
+ - Enable via `options.isAttachRawTombstone = true` or manifest: ``
- Add support to configure reporting historical ANRs via `AndroidManifest.xml` using the `io.sentry.anr.report-historical` attribute ([#5387](https://github.com/getsentry/sentry-java/pull/5387))
### Dependencies
diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api
index 3d4512fc2b4..249549f8366 100644
--- a/sentry-android-core/api/sentry-android-core.api
+++ b/sentry-android-core/api/sentry-android-core.api
@@ -374,6 +374,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun isAnrProfilingEnabled ()Z
public fun isAnrReportInDebug ()Z
public fun isAttachAnrThreadDump ()Z
+ public fun isAttachRawTombstone ()Z
public fun isAttachScreenshot ()Z
public fun isAttachViewHierarchy ()Z
public fun isCollectAdditionalContext ()Z
@@ -401,6 +402,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun setAnrReportInDebug (Z)V
public fun setAnrTimeoutIntervalMillis (J)V
public fun setAttachAnrThreadDump (Z)V
+ public fun setAttachRawTombstone (Z)V
public fun setAttachScreenshot (Z)V
public fun setAttachViewHierarchy (Z)V
public fun setBeforeScreenshotCaptureCallback (Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;)V
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 af3a942c8cc..8d88285a356 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
@@ -18,6 +18,7 @@
import io.sentry.android.core.cache.AndroidEnvelopeCache;
import io.sentry.android.core.internal.threaddump.Lines;
import io.sentry.android.core.internal.threaddump.ThreadDumpParser;
+import io.sentry.android.core.internal.util.NativeEventUtils;
import io.sentry.hints.AbnormalExit;
import io.sentry.hints.Backfillable;
import io.sentry.hints.BlockingFlushHint;
@@ -32,7 +33,6 @@
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;
@@ -194,7 +194,7 @@ public boolean shouldReportHistorical() {
if (trace == null) {
return new ParseResult(ParseResult.Type.NO_DUMP);
}
- dump = getDumpBytes(trace);
+ dump = NativeEventUtils.readBytes(trace);
} catch (Throwable e) {
options.getLogger().log(SentryLevel.WARNING, "Failed to read ANR thread dump", e);
return new ParseResult(ParseResult.Type.NO_DUMP);
@@ -223,20 +223,6 @@ public boolean shouldReportHistorical() {
return new ParseResult(ParseResult.Type.ERROR, dump);
}
}
-
- private byte[] getDumpBytes(final @NotNull InputStream trace) throws IOException {
- try (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 buffer.toByteArray();
- }
- }
}
@ApiStatus.Internal
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java
index b52634774d6..e16d4b312fc 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java
@@ -36,6 +36,7 @@ final class ManifestMetadataReader {
static final String ANR_REPORT_HISTORICAL = "io.sentry.anr.report-historical";
static final String TOMBSTONE_ENABLE = "io.sentry.tombstone.enable";
+ static final String TOMBSTONE_ATTACH_RAW = "io.sentry.tombstone.attach-raw";
static final String AUTO_INIT = "io.sentry.auto-init";
static final String NDK_ENABLE = "io.sentry.ndk.enable";
@@ -226,6 +227,8 @@ static void applyMetadata(
options.setAnrEnabled(readBool(metadata, logger, ANR_ENABLE, options.isAnrEnabled()));
options.setTombstoneEnabled(
readBool(metadata, logger, TOMBSTONE_ENABLE, options.isTombstoneEnabled()));
+ options.setAttachRawTombstone(
+ readBool(metadata, logger, TOMBSTONE_ATTACH_RAW, options.isAttachRawTombstone()));
// use enableAutoSessionTracking as fallback
options.setEnableAutoSessionTracking(
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 8fe702aad50..bb9ec17aabd 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
@@ -238,6 +238,12 @@ public interface BeforeCaptureCallback {
*/
private boolean attachAnrThreadDump = false;
+ /**
+ * Controls whether to attach the raw tombstone protobuf as an attachment. The tombstone is being
+ * attached from {@link ApplicationExitInfo#getTraceInputStream()}, if available.
+ */
+ private boolean attachRawTombstone = false;
+
private boolean enablePerformanceV2 = true;
private @Nullable SentryFrameMetricsCollector frameMetricsCollector;
@@ -643,6 +649,14 @@ public void setAttachAnrThreadDump(final boolean attachAnrThreadDump) {
this.attachAnrThreadDump = attachAnrThreadDump;
}
+ public boolean isAttachRawTombstone() {
+ return attachRawTombstone;
+ }
+
+ public void setAttachRawTombstone(final boolean attachRawTombstone) {
+ this.attachRawTombstone = attachRawTombstone;
+ }
+
/**
* @return true if performance-v2 is enabled. See {@link #setEnablePerformanceV2(boolean)} for
* more details.
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java
index f2b87742544..2663051f7e4 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java
@@ -24,6 +24,7 @@
import io.sentry.android.core.cache.AndroidEnvelopeCache;
import io.sentry.android.core.internal.tombstone.NativeExceptionMechanism;
import io.sentry.android.core.internal.tombstone.TombstoneParser;
+import io.sentry.android.core.internal.util.NativeEventUtils;
import io.sentry.hints.Backfillable;
import io.sentry.hints.BlockingFlushHint;
import io.sentry.hints.NativeCrashExit;
@@ -36,6 +37,7 @@
import io.sentry.transport.ICurrentDateProvider;
import io.sentry.util.HintUtils;
import io.sentry.util.Objects;
+import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
@@ -150,26 +152,35 @@ public boolean shouldReportHistorical() {
public @Nullable ApplicationExitInfoHistoryDispatcher.Report buildReport(
final @NotNull ApplicationExitInfo exitInfo, final boolean enrich) {
SentryEvent event;
+ @Nullable byte[] rawTombstone = null;
try {
- final InputStream tombstoneInputStream = exitInfo.getTraceInputStream();
- if (tombstoneInputStream == null) {
- options
- .getLogger()
- .log(
- SentryLevel.WARNING,
- "No tombstone InputStream available for ApplicationExitInfo from %s",
- DateTimeFormatter.ISO_INSTANT.format(
- Instant.ofEpochMilli(exitInfo.getTimestamp())));
- return null;
- }
+ final boolean attachRaw = options.isAttachRawTombstone();
+ try (final InputStream tombstoneInputStream = exitInfo.getTraceInputStream()) {
+ if (tombstoneInputStream == null) {
+ options
+ .getLogger()
+ .log(
+ SentryLevel.WARNING,
+ "No tombstone InputStream available for ApplicationExitInfo from %s",
+ DateTimeFormatter.ISO_INSTANT.format(
+ Instant.ofEpochMilli(exitInfo.getTimestamp())));
+ return null;
+ }
- try (final TombstoneParser parser =
- new TombstoneParser(
- tombstoneInputStream,
- this.options.getInAppIncludes(),
- this.options.getInAppExcludes(),
- this.context.getApplicationInfo().nativeLibraryDir)) {
- event = parser.parse();
+ if (attachRaw) {
+ rawTombstone = NativeEventUtils.readBytes(tombstoneInputStream);
+ }
+
+ final InputStream parserInput =
+ attachRaw ? new ByteArrayInputStream(rawTombstone) : tombstoneInputStream;
+ try (final TombstoneParser parser =
+ new TombstoneParser(
+ parserInput,
+ this.options.getInAppIncludes(),
+ this.options.getInAppExcludes(),
+ this.context.getApplicationInfo().nativeLibraryDir)) {
+ event = parser.parse();
+ }
}
} catch (Throwable e) {
options
@@ -190,6 +201,10 @@ public boolean shouldReportHistorical() {
options.getFlushTimeoutMillis(), options.getLogger(), tombstoneTimestamp, enrich);
final Hint hint = HintUtils.createWithTypeCheckHint(tombstoneHint);
+ if (rawTombstone != null) {
+ hint.setTombstone(Attachment.fromTombstone(rawTombstone));
+ }
+
try {
final @Nullable SentryEvent mergedEvent =
mergeWithMatchingNativeEvents(tombstoneTimestamp, event, hint);
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/NativeEventUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/NativeEventUtils.java
index f8bd70cb6c3..c5e766b3c44 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/NativeEventUtils.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/NativeEventUtils.java
@@ -1,5 +1,8 @@
package io.sentry.android.core.internal.util;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
import java.math.BigInteger;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
@@ -8,6 +11,18 @@
import org.jetbrains.annotations.Nullable;
public class NativeEventUtils {
+
+ public static byte[] readBytes(final @NotNull InputStream stream) throws IOException {
+ try (final ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
+ int nRead;
+ final byte[] data = new byte[1024];
+ while ((nRead = stream.read(data, 0, data.length)) != -1) {
+ buffer.write(data, 0, nRead);
+ }
+ return buffer.toByteArray();
+ }
+ }
+
@Nullable
public static String buildIdToDebugId(final @NotNull String buildId) {
try {
diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt
index cedf5ca18bb..d8ac959601a 100644
--- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt
+++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt
@@ -288,6 +288,31 @@ class ManifestMetadataReaderTest {
assertEquals(false, fixture.options.isAttachAnrThreadDump)
}
+ @Test
+ fun `applyMetadata reads tombstone attach raw to options`() {
+ // Arrange
+ val bundle = bundleOf(ManifestMetadataReader.TOMBSTONE_ATTACH_RAW to true)
+ val context = fixture.getContext(metaData = bundle)
+
+ // Act
+ ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
+
+ // Assert
+ assertEquals(true, fixture.options.isAttachRawTombstone)
+ }
+
+ @Test
+ fun `applyMetadata reads tombstone attach raw to options and keeps default`() {
+ // Arrange
+ val context = fixture.getContext()
+
+ // Act
+ ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
+
+ // Assert
+ assertEquals(false, fixture.options.isAttachRawTombstone)
+ }
+
@Test
fun `applyMetadata reads anr report historical to options`() {
// Arrange
diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt
index 3b27d69d087..9890d553dbc 100644
--- a/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt
+++ b/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt
@@ -96,6 +96,45 @@ class TombstoneIntegrationTest : ApplicationExitIntegrationTestBase
+ options.isAttachRawTombstone = true
+ }
+
+ fixture.addAppExitInfo(timestamp = newTimestamp)
+
+ integration.register(fixture.scopes, fixture.options)
+
+ verify(fixture.scopes)
+ .captureEvent(
+ any(),
+ argThat {
+ val tombstone = this.tombstone
+ tombstone != null &&
+ tombstone.filename == "tombstone.pb" &&
+ tombstone.contentType == "application/x-protobuf" &&
+ tombstone.bytes != null &&
+ tombstone.bytes!!.isNotEmpty()
+ },
+ )
+ }
+
+ @Test
+ fun `when attachRawTombstone is disabled, no tombstone is attached to hint`() {
+ val integration =
+ fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp) { options ->
+ options.isAttachRawTombstone = false
+ }
+
+ fixture.addAppExitInfo(timestamp = newTimestamp)
+
+ integration.register(fixture.scopes, fixture.options)
+
+ verify(fixture.scopes).captureEvent(any(), argThat { this.tombstone == null })
+ }
+
@Test
fun `when matching native event has attachments, they are added to the hint`() {
val integration =
diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api
index a433abbb37c..6bef3fe3c11 100644
--- a/sentry/api/sentry.api
+++ b/sentry/api/sentry.api
@@ -19,6 +19,7 @@ public final class io/sentry/Attachment {
public static fun fromByteProvider (Ljava/util/concurrent/Callable;Ljava/lang/String;Ljava/lang/String;Z)Lio/sentry/Attachment;
public static fun fromScreenshot ([B)Lio/sentry/Attachment;
public static fun fromThreadDump ([B)Lio/sentry/Attachment;
+ public static fun fromTombstone ([B)Lio/sentry/Attachment;
public static fun fromViewHierarchy (Lio/sentry/protocol/ViewHierarchy;)Lio/sentry/Attachment;
public fun getAttachmentType ()Ljava/lang/String;
public fun getByteProvider ()Ljava/util/concurrent/Callable;
@@ -613,6 +614,7 @@ public final class io/sentry/Hint {
public fun getReplayRecording ()Lio/sentry/ReplayRecording;
public fun getScreenshot ()Lio/sentry/Attachment;
public fun getThreadDump ()Lio/sentry/Attachment;
+ public fun getTombstone ()Lio/sentry/Attachment;
public fun getViewHierarchy ()Lio/sentry/Attachment;
public fun remove (Ljava/lang/String;)V
public fun replaceAttachments (Ljava/util/List;)V
@@ -620,6 +622,7 @@ public final class io/sentry/Hint {
public fun setReplayRecording (Lio/sentry/ReplayRecording;)V
public fun setScreenshot (Lio/sentry/Attachment;)V
public fun setThreadDump (Lio/sentry/Attachment;)V
+ public fun setTombstone (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 439ad812b0c..3e4cb859e5e 100644
--- a/sentry/src/main/java/io/sentry/Attachment.java
+++ b/sentry/src/main/java/io/sentry/Attachment.java
@@ -396,4 +396,14 @@ boolean isAddToTransactions() {
public static @NotNull Attachment fromThreadDump(final byte[] bytes) {
return new Attachment(bytes, "thread-dump.txt", "text/plain", false);
}
+
+ /**
+ * Creates a new Tombstone Attachment
+ *
+ * @param bytes the array bytes
+ * @return the Attachment
+ */
+ public static @NotNull Attachment fromTombstone(final byte[] bytes) {
+ return new Attachment(bytes, "tombstone.pb", "application/x-protobuf", false);
+ }
}
diff --git a/sentry/src/main/java/io/sentry/Hint.java b/sentry/src/main/java/io/sentry/Hint.java
index d7949b3133b..1e09dca5541 100644
--- a/sentry/src/main/java/io/sentry/Hint.java
+++ b/sentry/src/main/java/io/sentry/Hint.java
@@ -32,6 +32,7 @@ public final class Hint {
private @Nullable Attachment screenshot = null;
private @Nullable Attachment viewHierarchy = null;
private @Nullable Attachment threadDump = null;
+ private @Nullable Attachment tombstone = null;
private @Nullable ReplayRecording replayRecording = null;
public static @NotNull Hint withAttachment(@Nullable Attachment attachment) {
@@ -147,6 +148,14 @@ public void setThreadDump(final @Nullable Attachment threadDump) {
return threadDump;
}
+ public void setTombstone(final @Nullable Attachment tombstone) {
+ this.tombstone = tombstone;
+ }
+
+ public @Nullable Attachment getTombstone() {
+ return tombstone;
+ }
+
@Nullable
public ReplayRecording getReplayRecording() {
return replayRecording;
diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java
index c99fcaeaa2f..6f328d0fd58 100644
--- a/sentry/src/main/java/io/sentry/SentryClient.java
+++ b/sentry/src/main/java/io/sentry/SentryClient.java
@@ -399,6 +399,11 @@ private boolean shouldSendSessionUpdateForDroppedEvent(
attachments.add(threadDump);
}
+ @Nullable final Attachment tombstone = hint.getTombstone();
+ if (tombstone != null) {
+ attachments.add(tombstone);
+ }
+
return attachments;
}
diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt
index 11ff80fd573..663b1f9bdee 100644
--- a/sentry/src/test/java/io/sentry/SentryClientTest.kt
+++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt
@@ -2124,6 +2124,37 @@ class SentryClientTest {
.send(check { envelope -> assertEquals(1, envelope.items.count()) }, anyOrNull())
}
+ @Test
+ fun `tombstone is added to the envelope from the hint`() {
+ val sut = fixture.getSut()
+ val attachment = Attachment.fromTombstone(byteArrayOf())
+ val hint = Hint().also { it.tombstone = attachment }
+
+ sut.captureEvent(SentryEvent(), hint)
+
+ verify(fixture.transport)
+ .send(
+ check { envelope ->
+ val tombstone = envelope.items.last()
+ assertNotNull(tombstone) { assertEquals(attachment.filename, tombstone.header.fileName) }
+ },
+ anyOrNull(),
+ )
+ }
+
+ @Test
+ fun `tombstone is dropped from hint via before send`() {
+ fixture.sentryOptions.beforeSend = CustomBeforeSendCallback()
+ val sut = fixture.getSut()
+ val attachment = Attachment.fromTombstone(byteArrayOf())
+ val hint = Hint().also { it.tombstone = 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()
@@ -3647,6 +3678,7 @@ class SentryClientTest {
hint.screenshot = null
hint.viewHierarchy = null
hint.threadDump = null
+ hint.tombstone = 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 7be03e7dd67..7b0c695bfd4 100644
--- a/sentry/src/test/java/io/sentry/hints/HintTest.kt
+++ b/sentry/src/test/java/io/sentry/hints/HintTest.kt
@@ -210,6 +210,7 @@ class HintTest {
hint.screenshot = newAttachment("2")
hint.viewHierarchy = newAttachment("3")
hint.threadDump = newAttachment("4")
+ hint.tombstone = newAttachment("5")
hint.clear()
@@ -219,6 +220,7 @@ class HintTest {
assertNotNull(hint.screenshot)
assertNotNull(hint.viewHierarchy)
assertNotNull(hint.threadDump)
+ assertNotNull(hint.tombstone)
}
@Test
@@ -248,6 +250,15 @@ class HintTest {
assertNotNull(hint.threadDump)
}
+ @Test
+ fun `can create hint with a tombstone`() {
+ val hint = Hint()
+ val attachment = newAttachment("tombstone")
+ hint.tombstone = attachment
+
+ assertNotNull(hint.tombstone)
+ }
+
companion object {
fun newAttachment(content: String) = Attachment(content.toByteArray(), "$content.txt")
}