Skip to content

Commit 69508a1

Browse files
romtsnclaudegetsentry-bot
authored
feat(tombstones): Add option to attach raw tombstone as protobuf (#5446)
* feat(android): Add option to attach raw tombstone as protobuf Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * changelog * changelog * ref(android): Rename attachTombstone to attachRawTombstone Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(android): Add tests for raw tombstone attachment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Format code * fix(android): Close tombstone InputStream after reading bytes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ref(android): Extract shared readBytes into NativeEventUtils Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(android): Fix JavaDoc and address review feedback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ref(android): Only pre-buffer tombstone bytes when attach option is on Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
1 parent 11f90db commit 69508a1

15 files changed

Lines changed: 205 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features
66

7+
- Add option to attach raw tombstone protobuf on native crash events ([#5446](https://github.com/getsentry/sentry-java/pull/5446))
8+
- Enable via `options.isAttachRawTombstone = true` or manifest: `<meta-data android:name="io.sentry.tombstone.attach-raw" android:value="true" />`
79
- Add API to clear feature flags from scopes ([#5426](https://github.com/getsentry/sentry-java/pull/5426))
810
- 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))
911

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
374374
public fun isAnrProfilingEnabled ()Z
375375
public fun isAnrReportInDebug ()Z
376376
public fun isAttachAnrThreadDump ()Z
377+
public fun isAttachRawTombstone ()Z
377378
public fun isAttachScreenshot ()Z
378379
public fun isAttachViewHierarchy ()Z
379380
public fun isCollectAdditionalContext ()Z
@@ -401,6 +402,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
401402
public fun setAnrReportInDebug (Z)V
402403
public fun setAnrTimeoutIntervalMillis (J)V
403404
public fun setAttachAnrThreadDump (Z)V
405+
public fun setAttachRawTombstone (Z)V
404406
public fun setAttachScreenshot (Z)V
405407
public fun setAttachViewHierarchy (Z)V
406408
public fun setBeforeScreenshotCaptureCallback (Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;)V

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

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import io.sentry.android.core.cache.AndroidEnvelopeCache;
1919
import io.sentry.android.core.internal.threaddump.Lines;
2020
import io.sentry.android.core.internal.threaddump.ThreadDumpParser;
21+
import io.sentry.android.core.internal.util.NativeEventUtils;
2122
import io.sentry.hints.AbnormalExit;
2223
import io.sentry.hints.Backfillable;
2324
import io.sentry.hints.BlockingFlushHint;
@@ -32,7 +33,6 @@
3233
import io.sentry.util.Objects;
3334
import java.io.BufferedReader;
3435
import java.io.ByteArrayInputStream;
35-
import java.io.ByteArrayOutputStream;
3636
import java.io.Closeable;
3737
import java.io.IOException;
3838
import java.io.InputStream;
@@ -194,7 +194,7 @@ public boolean shouldReportHistorical() {
194194
if (trace == null) {
195195
return new ParseResult(ParseResult.Type.NO_DUMP);
196196
}
197-
dump = getDumpBytes(trace);
197+
dump = NativeEventUtils.readBytes(trace);
198198
} catch (Throwable e) {
199199
options.getLogger().log(SentryLevel.WARNING, "Failed to read ANR thread dump", e);
200200
return new ParseResult(ParseResult.Type.NO_DUMP);
@@ -223,20 +223,6 @@ public boolean shouldReportHistorical() {
223223
return new ParseResult(ParseResult.Type.ERROR, dump);
224224
}
225225
}
226-
227-
private byte[] getDumpBytes(final @NotNull InputStream trace) throws IOException {
228-
try (final ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
229-
230-
int nRead;
231-
final byte[] data = new byte[1024];
232-
233-
while ((nRead = trace.read(data, 0, data.length)) != -1) {
234-
buffer.write(data, 0, nRead);
235-
}
236-
237-
return buffer.toByteArray();
238-
}
239-
}
240226
}
241227

242228
@ApiStatus.Internal

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ final class ManifestMetadataReader {
3636
static final String ANR_REPORT_HISTORICAL = "io.sentry.anr.report-historical";
3737

3838
static final String TOMBSTONE_ENABLE = "io.sentry.tombstone.enable";
39+
static final String TOMBSTONE_ATTACH_RAW = "io.sentry.tombstone.attach-raw";
3940

4041
static final String AUTO_INIT = "io.sentry.auto-init";
4142
static final String NDK_ENABLE = "io.sentry.ndk.enable";
@@ -226,6 +227,8 @@ static void applyMetadata(
226227
options.setAnrEnabled(readBool(metadata, logger, ANR_ENABLE, options.isAnrEnabled()));
227228
options.setTombstoneEnabled(
228229
readBool(metadata, logger, TOMBSTONE_ENABLE, options.isTombstoneEnabled()));
230+
options.setAttachRawTombstone(
231+
readBool(metadata, logger, TOMBSTONE_ATTACH_RAW, options.isAttachRawTombstone()));
229232

230233
// use enableAutoSessionTracking as fallback
231234
options.setEnableAutoSessionTracking(

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,12 @@ public interface BeforeCaptureCallback {
238238
*/
239239
private boolean attachAnrThreadDump = false;
240240

241+
/**
242+
* Controls whether to attach the raw tombstone protobuf as an attachment. The tombstone is being
243+
* attached from {@link ApplicationExitInfo#getTraceInputStream()}, if available.
244+
*/
245+
private boolean attachRawTombstone = false;
246+
241247
private boolean enablePerformanceV2 = true;
242248

243249
private @Nullable SentryFrameMetricsCollector frameMetricsCollector;
@@ -643,6 +649,14 @@ public void setAttachAnrThreadDump(final boolean attachAnrThreadDump) {
643649
this.attachAnrThreadDump = attachAnrThreadDump;
644650
}
645651

652+
public boolean isAttachRawTombstone() {
653+
return attachRawTombstone;
654+
}
655+
656+
public void setAttachRawTombstone(final boolean attachRawTombstone) {
657+
this.attachRawTombstone = attachRawTombstone;
658+
}
659+
646660
/**
647661
* @return true if performance-v2 is enabled. See {@link #setEnablePerformanceV2(boolean)} for
648662
* more details.

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

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.sentry.android.core.cache.AndroidEnvelopeCache;
2525
import io.sentry.android.core.internal.tombstone.NativeExceptionMechanism;
2626
import io.sentry.android.core.internal.tombstone.TombstoneParser;
27+
import io.sentry.android.core.internal.util.NativeEventUtils;
2728
import io.sentry.hints.Backfillable;
2829
import io.sentry.hints.BlockingFlushHint;
2930
import io.sentry.hints.NativeCrashExit;
@@ -36,6 +37,7 @@
3637
import io.sentry.transport.ICurrentDateProvider;
3738
import io.sentry.util.HintUtils;
3839
import io.sentry.util.Objects;
40+
import java.io.ByteArrayInputStream;
3941
import java.io.Closeable;
4042
import java.io.IOException;
4143
import java.io.InputStream;
@@ -150,26 +152,35 @@ public boolean shouldReportHistorical() {
150152
public @Nullable ApplicationExitInfoHistoryDispatcher.Report buildReport(
151153
final @NotNull ApplicationExitInfo exitInfo, final boolean enrich) {
152154
SentryEvent event;
155+
@Nullable byte[] rawTombstone = null;
153156
try {
154-
final InputStream tombstoneInputStream = exitInfo.getTraceInputStream();
155-
if (tombstoneInputStream == null) {
156-
options
157-
.getLogger()
158-
.log(
159-
SentryLevel.WARNING,
160-
"No tombstone InputStream available for ApplicationExitInfo from %s",
161-
DateTimeFormatter.ISO_INSTANT.format(
162-
Instant.ofEpochMilli(exitInfo.getTimestamp())));
163-
return null;
164-
}
157+
final boolean attachRaw = options.isAttachRawTombstone();
158+
try (final InputStream tombstoneInputStream = exitInfo.getTraceInputStream()) {
159+
if (tombstoneInputStream == null) {
160+
options
161+
.getLogger()
162+
.log(
163+
SentryLevel.WARNING,
164+
"No tombstone InputStream available for ApplicationExitInfo from %s",
165+
DateTimeFormatter.ISO_INSTANT.format(
166+
Instant.ofEpochMilli(exitInfo.getTimestamp())));
167+
return null;
168+
}
165169

166-
try (final TombstoneParser parser =
167-
new TombstoneParser(
168-
tombstoneInputStream,
169-
this.options.getInAppIncludes(),
170-
this.options.getInAppExcludes(),
171-
this.context.getApplicationInfo().nativeLibraryDir)) {
172-
event = parser.parse();
170+
if (attachRaw) {
171+
rawTombstone = NativeEventUtils.readBytes(tombstoneInputStream);
172+
}
173+
174+
final InputStream parserInput =
175+
attachRaw ? new ByteArrayInputStream(rawTombstone) : tombstoneInputStream;
176+
try (final TombstoneParser parser =
177+
new TombstoneParser(
178+
parserInput,
179+
this.options.getInAppIncludes(),
180+
this.options.getInAppExcludes(),
181+
this.context.getApplicationInfo().nativeLibraryDir)) {
182+
event = parser.parse();
183+
}
173184
}
174185
} catch (Throwable e) {
175186
options
@@ -190,6 +201,10 @@ public boolean shouldReportHistorical() {
190201
options.getFlushTimeoutMillis(), options.getLogger(), tombstoneTimestamp, enrich);
191202
final Hint hint = HintUtils.createWithTypeCheckHint(tombstoneHint);
192203

204+
if (rawTombstone != null) {
205+
hint.setTombstone(Attachment.fromTombstone(rawTombstone));
206+
}
207+
193208
try {
194209
final @Nullable SentryEvent mergedEvent =
195210
mergeWithMatchingNativeEvents(tombstoneTimestamp, event, hint);

sentry-android-core/src/main/java/io/sentry/android/core/internal/util/NativeEventUtils.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package io.sentry.android.core.internal.util;
22

3+
import java.io.ByteArrayOutputStream;
4+
import java.io.IOException;
5+
import java.io.InputStream;
36
import java.math.BigInteger;
47
import java.nio.BufferUnderflowException;
58
import java.nio.ByteBuffer;
@@ -8,6 +11,18 @@
811
import org.jetbrains.annotations.Nullable;
912

1013
public class NativeEventUtils {
14+
15+
public static byte[] readBytes(final @NotNull InputStream stream) throws IOException {
16+
try (final ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
17+
int nRead;
18+
final byte[] data = new byte[1024];
19+
while ((nRead = stream.read(data, 0, data.length)) != -1) {
20+
buffer.write(data, 0, nRead);
21+
}
22+
return buffer.toByteArray();
23+
}
24+
}
25+
1126
@Nullable
1227
public static String buildIdToDebugId(final @NotNull String buildId) {
1328
try {

sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,31 @@ class ManifestMetadataReaderTest {
288288
assertEquals(false, fixture.options.isAttachAnrThreadDump)
289289
}
290290

291+
@Test
292+
fun `applyMetadata reads tombstone attach raw to options`() {
293+
// Arrange
294+
val bundle = bundleOf(ManifestMetadataReader.TOMBSTONE_ATTACH_RAW to true)
295+
val context = fixture.getContext(metaData = bundle)
296+
297+
// Act
298+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
299+
300+
// Assert
301+
assertEquals(true, fixture.options.isAttachRawTombstone)
302+
}
303+
304+
@Test
305+
fun `applyMetadata reads tombstone attach raw to options and keeps default`() {
306+
// Arrange
307+
val context = fixture.getContext()
308+
309+
// Act
310+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
311+
312+
// Assert
313+
assertEquals(false, fixture.options.isAttachRawTombstone)
314+
}
315+
291316
@Test
292317
fun `applyMetadata reads anr report historical to options`() {
293318
// Arrange

sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,45 @@ class TombstoneIntegrationTest : ApplicationExitIntegrationTestBase<TombstoneHin
9696
assertEquals(57344, image.imageSize)
9797
}
9898

99+
@Test
100+
fun `when attachRawTombstone is enabled, raw tombstone is attached to hint`() {
101+
val integration =
102+
fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp) { options ->
103+
options.isAttachRawTombstone = true
104+
}
105+
106+
fixture.addAppExitInfo(timestamp = newTimestamp)
107+
108+
integration.register(fixture.scopes, fixture.options)
109+
110+
verify(fixture.scopes)
111+
.captureEvent(
112+
any(),
113+
argThat<Hint> {
114+
val tombstone = this.tombstone
115+
tombstone != null &&
116+
tombstone.filename == "tombstone.pb" &&
117+
tombstone.contentType == "application/x-protobuf" &&
118+
tombstone.bytes != null &&
119+
tombstone.bytes!!.isNotEmpty()
120+
},
121+
)
122+
}
123+
124+
@Test
125+
fun `when attachRawTombstone is disabled, no tombstone is attached to hint`() {
126+
val integration =
127+
fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp) { options ->
128+
options.isAttachRawTombstone = false
129+
}
130+
131+
fixture.addAppExitInfo(timestamp = newTimestamp)
132+
133+
integration.register(fixture.scopes, fixture.options)
134+
135+
verify(fixture.scopes).captureEvent(any(), argThat<Hint> { this.tombstone == null })
136+
}
137+
99138
@Test
100139
fun `when matching native event has attachments, they are added to the hint`() {
101140
val integration =

sentry/api/sentry.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public final class io/sentry/Attachment {
1919
public static fun fromByteProvider (Ljava/util/concurrent/Callable;Ljava/lang/String;Ljava/lang/String;Z)Lio/sentry/Attachment;
2020
public static fun fromScreenshot ([B)Lio/sentry/Attachment;
2121
public static fun fromThreadDump ([B)Lio/sentry/Attachment;
22+
public static fun fromTombstone ([B)Lio/sentry/Attachment;
2223
public static fun fromViewHierarchy (Lio/sentry/protocol/ViewHierarchy;)Lio/sentry/Attachment;
2324
public fun getAttachmentType ()Ljava/lang/String;
2425
public fun getByteProvider ()Ljava/util/concurrent/Callable;
@@ -614,13 +615,15 @@ public final class io/sentry/Hint {
614615
public fun getReplayRecording ()Lio/sentry/ReplayRecording;
615616
public fun getScreenshot ()Lio/sentry/Attachment;
616617
public fun getThreadDump ()Lio/sentry/Attachment;
618+
public fun getTombstone ()Lio/sentry/Attachment;
617619
public fun getViewHierarchy ()Lio/sentry/Attachment;
618620
public fun remove (Ljava/lang/String;)V
619621
public fun replaceAttachments (Ljava/util/List;)V
620622
public fun set (Ljava/lang/String;Ljava/lang/Object;)V
621623
public fun setReplayRecording (Lio/sentry/ReplayRecording;)V
622624
public fun setScreenshot (Lio/sentry/Attachment;)V
623625
public fun setThreadDump (Lio/sentry/Attachment;)V
626+
public fun setTombstone (Lio/sentry/Attachment;)V
624627
public fun setViewHierarchy (Lio/sentry/Attachment;)V
625628
public static fun withAttachment (Lio/sentry/Attachment;)Lio/sentry/Hint;
626629
public static fun withAttachments (Ljava/util/List;)Lio/sentry/Hint;

0 commit comments

Comments
 (0)