From e16e381f48b3879622a4826d45aa15d327887188 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 18 May 2026 15:49:16 +0200 Subject: [PATCH 01/10] feat(android): Add option to attach raw tombstone as protobuf Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/sentry-android-core.api | 2 ++ .../android/core/ManifestMetadataReader.java | 3 +++ .../android/core/SentryAndroidOptions.java | 14 ++++++++++++ .../android/core/TombstoneIntegration.java | 22 ++++++++++++++++++- sentry/api/sentry.api | 3 +++ .../src/main/java/io/sentry/Attachment.java | 10 +++++++++ sentry/src/main/java/io/sentry/Hint.java | 9 ++++++++ .../src/main/java/io/sentry/SentryClient.java | 5 +++++ 8 files changed, 67 insertions(+), 1 deletion(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 3d4512fc2b4..3694f33c03a 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -375,6 +375,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun isAnrReportInDebug ()Z public fun isAttachAnrThreadDump ()Z public fun isAttachScreenshot ()Z + public fun isAttachTombstone ()Z public fun isAttachViewHierarchy ()Z public fun isCollectAdditionalContext ()Z public fun isCollectExternalStorageContext ()Z @@ -402,6 +403,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setAnrTimeoutIntervalMillis (J)V public fun setAttachAnrThreadDump (Z)V public fun setAttachScreenshot (Z)V + public fun setAttachTombstone (Z)V public fun setAttachViewHierarchy (Z)V public fun setBeforeScreenshotCaptureCallback (Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;)V public fun setBeforeViewHierarchyCaptureCallback (Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;)V 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..270cc19ee34 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 = "io.sentry.tombstone.attach"; 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.setAttachTombstone( + readBool(metadata, logger, TOMBSTONE_ATTACH, options.isAttachTombstone())); // 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..dfe4d27154a 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 send raw tombstone as an attachment with plain text. The tombstone is being + * attached from {@link ApplicationExitInfo#getTraceInputStream()}, if available. + */ + private boolean attachTombstone = false; + private boolean enablePerformanceV2 = true; private @Nullable SentryFrameMetricsCollector frameMetricsCollector; @@ -643,6 +649,14 @@ public void setAttachAnrThreadDump(final boolean attachAnrThreadDump) { this.attachAnrThreadDump = attachAnrThreadDump; } + public boolean isAttachTombstone() { + return attachTombstone; + } + + public void setAttachTombstone(final boolean attachTombstone) { + this.attachTombstone = attachTombstone; + } + /** * @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..9ebb2f5d164 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 @@ -36,6 +36,8 @@ import io.sentry.transport.ICurrentDateProvider; import io.sentry.util.HintUtils; import io.sentry.util.Objects; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; @@ -150,6 +152,7 @@ public boolean shouldReportHistorical() { public @Nullable ApplicationExitInfoHistoryDispatcher.Report buildReport( final @NotNull ApplicationExitInfo exitInfo, final boolean enrich) { SentryEvent event; + final byte[] rawTombstone; try { final InputStream tombstoneInputStream = exitInfo.getTraceInputStream(); if (tombstoneInputStream == null) { @@ -163,9 +166,11 @@ public boolean shouldReportHistorical() { return null; } + rawTombstone = readBytes(tombstoneInputStream); + try (final TombstoneParser parser = new TombstoneParser( - tombstoneInputStream, + new ByteArrayInputStream(rawTombstone), this.options.getInAppIncludes(), this.options.getInAppExcludes(), this.context.getApplicationInfo().nativeLibraryDir)) { @@ -190,6 +195,10 @@ public boolean shouldReportHistorical() { options.getFlushTimeoutMillis(), options.getLogger(), tombstoneTimestamp, enrich); final Hint hint = HintUtils.createWithTypeCheckHint(tombstoneHint); + if (options.isAttachTombstone()) { + hint.setTombstone(Attachment.fromTombstone(rawTombstone)); + } + try { final @Nullable SentryEvent mergedEvent = mergeWithMatchingNativeEvents(tombstoneTimestamp, event, hint); @@ -304,6 +313,17 @@ private void mergeNativeCrashes( nativeEvent.setThreads(tombstoneThreads); } } + + private 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(); + } + } } @ApiStatus.Internal 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; } From 200aefc44deab8efc8a3d5365b94ed70651da07e Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 18 May 2026 15:49:58 +0200 Subject: [PATCH 02/10] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5632b6926b9..7ccf3719835 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Add option to attach raw tombstone protobuf on native crash events ([#5446](https://github.com/getsentry/sentry-java/pull/5446)) - 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 From 322a294a968aec5393da2bea9dab2882ebe1377b Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 18 May 2026 15:50:56 +0200 Subject: [PATCH 03/10] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ccf3719835..7aaeb34da93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Add option to attach raw tombstone protobuf on native crash events ([#5446](https://github.com/getsentry/sentry-java/pull/5446)) + - Enable via `options.isAttachTombstone = 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 From 52914993a7276aa9b369501e49061efb7ab8cf27 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 18 May 2026 22:11:36 +0200 Subject: [PATCH 04/10] ref(android): Rename attachTombstone to attachRawTombstone Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 2 +- sentry-android-core/api/sentry-android-core.api | 4 ++-- .../io/sentry/android/core/ManifestMetadataReader.java | 6 +++--- .../io/sentry/android/core/SentryAndroidOptions.java | 10 +++++----- .../io/sentry/android/core/TombstoneIntegration.java | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aaeb34da93..7faa69032a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Features - Add option to attach raw tombstone protobuf on native crash events ([#5446](https://github.com/getsentry/sentry-java/pull/5446)) - - Enable via `options.isAttachTombstone = true` or manifest: `` + - 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 3694f33c03a..249549f8366 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -374,8 +374,8 @@ 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 isAttachTombstone ()Z public fun isAttachViewHierarchy ()Z public fun isCollectAdditionalContext ()Z public fun isCollectExternalStorageContext ()Z @@ -402,8 +402,8 @@ 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 setAttachTombstone (Z)V public fun setAttachViewHierarchy (Z)V public fun setBeforeScreenshotCaptureCallback (Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;)V public fun setBeforeViewHierarchyCaptureCallback (Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;)V 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 270cc19ee34..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,7 +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 = "io.sentry.tombstone.attach"; + 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"; @@ -227,8 +227,8 @@ static void applyMetadata( options.setAnrEnabled(readBool(metadata, logger, ANR_ENABLE, options.isAnrEnabled())); options.setTombstoneEnabled( readBool(metadata, logger, TOMBSTONE_ENABLE, options.isTombstoneEnabled())); - options.setAttachTombstone( - readBool(metadata, logger, TOMBSTONE_ATTACH, options.isAttachTombstone())); + 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 dfe4d27154a..c01d0b34a2b 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 @@ -242,7 +242,7 @@ public interface BeforeCaptureCallback { * Controls whether to send raw tombstone as an attachment with plain text. The tombstone is being * attached from {@link ApplicationExitInfo#getTraceInputStream()}, if available. */ - private boolean attachTombstone = false; + private boolean attachRawTombstone = false; private boolean enablePerformanceV2 = true; @@ -649,12 +649,12 @@ public void setAttachAnrThreadDump(final boolean attachAnrThreadDump) { this.attachAnrThreadDump = attachAnrThreadDump; } - public boolean isAttachTombstone() { - return attachTombstone; + public boolean isAttachRawTombstone() { + return attachRawTombstone; } - public void setAttachTombstone(final boolean attachTombstone) { - this.attachTombstone = attachTombstone; + public void setAttachRawTombstone(final boolean attachRawTombstone) { + this.attachRawTombstone = attachRawTombstone; } /** 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 9ebb2f5d164..ea30e879ff5 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 @@ -195,7 +195,7 @@ public boolean shouldReportHistorical() { options.getFlushTimeoutMillis(), options.getLogger(), tombstoneTimestamp, enrich); final Hint hint = HintUtils.createWithTypeCheckHint(tombstoneHint); - if (options.isAttachTombstone()) { + if (options.isAttachRawTombstone()) { hint.setTombstone(Attachment.fromTombstone(rawTombstone)); } From 823676e85cb39294a5e9e65a978cf5508a54940f Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 18 May 2026 22:18:14 +0200 Subject: [PATCH 05/10] test(android): Add tests for raw tombstone attachment Co-Authored-By: Claude Opus 4.6 (1M context) --- .../core/ManifestMetadataReaderTest.kt | 25 +++++++++++ .../android/core/TombstoneIntegrationTest.kt | 43 +++++++++++++++++++ .../test/java/io/sentry/SentryClientTest.kt | 34 +++++++++++++++ .../src/test/java/io/sentry/hints/HintTest.kt | 11 +++++ 4 files changed, 113 insertions(+) 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..0597a493595 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,49 @@ 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/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 11ff80fd573..9ba761e4618 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -2124,6 +2124,39 @@ 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 +3680,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") } From 9a4963ebdc3b26b2cd24308818aaacebde13bcb7 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 18 May 2026 20:21:37 +0000 Subject: [PATCH 06/10] Format code --- .../java/io/sentry/android/core/TombstoneIntegrationTest.kt | 6 +----- sentry/src/test/java/io/sentry/SentryClientTest.kt | 4 +--- 2 files changed, 2 insertions(+), 8 deletions(-) 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 0597a493595..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 @@ -132,11 +132,7 @@ class TombstoneIntegrationTest : ApplicationExitIntegrationTestBase { this.tombstone == null }, - ) + verify(fixture.scopes).captureEvent(any(), argThat { this.tombstone == null }) } @Test diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 9ba761e4618..663b1f9bdee 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -2136,9 +2136,7 @@ class SentryClientTest { .send( check { envelope -> val tombstone = envelope.items.last() - assertNotNull(tombstone) { - assertEquals(attachment.filename, tombstone.header.fileName) - } + assertNotNull(tombstone) { assertEquals(attachment.filename, tombstone.header.fileName) } }, anyOrNull(), ) From a1af456e5d5f6853fb36e04ce63bd803187fa396 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 18 May 2026 22:30:04 +0200 Subject: [PATCH 07/10] fix(android): Close tombstone InputStream after reading bytes Co-Authored-By: Claude Opus 4.6 (1M context) --- .../android/core/TombstoneIntegration.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) 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 ea30e879ff5..0e5df266072 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 @@ -154,19 +154,20 @@ public boolean shouldReportHistorical() { SentryEvent event; final byte[] rawTombstone; 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 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; + } - rawTombstone = readBytes(tombstoneInputStream); + rawTombstone = readBytes(tombstoneInputStream); + } try (final TombstoneParser parser = new TombstoneParser( From 8a1c5ba75f335336345955f67c7be1ed80ccdfaa Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 18 May 2026 22:32:57 +0200 Subject: [PATCH 08/10] ref(android): Extract shared readBytes into NativeEventUtils Co-Authored-By: Claude Opus 4.6 (1M context) --- .../sentry/android/core/AnrV2Integration.java | 18 ++---------------- .../android/core/TombstoneIntegration.java | 15 ++------------- .../core/internal/util/NativeEventUtils.java | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 29 deletions(-) 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/TombstoneIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java index 0e5df266072..e876c7aaecb 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; @@ -37,7 +38,6 @@ import io.sentry.util.HintUtils; import io.sentry.util.Objects; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; @@ -166,7 +166,7 @@ public boolean shouldReportHistorical() { return null; } - rawTombstone = readBytes(tombstoneInputStream); + rawTombstone = NativeEventUtils.readBytes(tombstoneInputStream); } try (final TombstoneParser parser = @@ -314,17 +314,6 @@ private void mergeNativeCrashes( nativeEvent.setThreads(tombstoneThreads); } } - - private 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(); - } - } } @ApiStatus.Internal 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 { From be90d3053cf818722933d93300257320a5fc4b18 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 19 May 2026 22:14:12 +0200 Subject: [PATCH 09/10] fix(android): Fix JavaDoc and address review feedback Co-Authored-By: Claude Opus 4.6 (1M context) --- .../main/java/io/sentry/android/core/SentryAndroidOptions.java | 2 +- .../main/java/io/sentry/android/core/TombstoneIntegration.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 c01d0b34a2b..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 @@ -239,7 +239,7 @@ public interface BeforeCaptureCallback { private boolean attachAnrThreadDump = false; /** - * Controls whether to send raw tombstone as an attachment with plain text. The tombstone is being + * 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; 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 e876c7aaecb..23c47bc23af 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 @@ -152,7 +152,7 @@ public boolean shouldReportHistorical() { public @Nullable ApplicationExitInfoHistoryDispatcher.Report buildReport( final @NotNull ApplicationExitInfo exitInfo, final boolean enrich) { SentryEvent event; - final byte[] rawTombstone; + @Nullable byte[] rawTombstone = null; try { try (final InputStream tombstoneInputStream = exitInfo.getTraceInputStream()) { if (tombstoneInputStream == null) { From 169ac4fedab460d8c871ad384b6c9ffc1e5dbb1a Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 19 May 2026 22:21:41 +0200 Subject: [PATCH 10/10] ref(android): Only pre-buffer tombstone bytes when attach option is on Co-Authored-By: Claude Opus 4.6 (1M context) --- .../android/core/TombstoneIntegration.java | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) 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 23c47bc23af..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 @@ -154,6 +154,7 @@ public boolean shouldReportHistorical() { SentryEvent event; @Nullable byte[] rawTombstone = null; try { + final boolean attachRaw = options.isAttachRawTombstone(); try (final InputStream tombstoneInputStream = exitInfo.getTraceInputStream()) { if (tombstoneInputStream == null) { options @@ -166,16 +167,20 @@ public boolean shouldReportHistorical() { return null; } - rawTombstone = NativeEventUtils.readBytes(tombstoneInputStream); - } + if (attachRaw) { + rawTombstone = NativeEventUtils.readBytes(tombstoneInputStream); + } - try (final TombstoneParser parser = - new TombstoneParser( - new ByteArrayInputStream(rawTombstone), - this.options.getInAppIncludes(), - this.options.getInAppExcludes(), - this.context.getApplicationInfo().nativeLibraryDir)) { - event = parser.parse(); + 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 @@ -196,7 +201,7 @@ public boolean shouldReportHistorical() { options.getFlushTimeoutMillis(), options.getLogger(), tombstoneTimestamp, enrich); final Hint hint = HintUtils.createWithTypeCheckHint(tombstoneHint); - if (options.isAttachRawTombstone()) { + if (rawTombstone != null) { hint.setTombstone(Attachment.fromTombstone(rawTombstone)); }