Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<meta-data android:name="io.sentry.tombstone.attach-raw" android:value="true" />`
- 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
Expand Down
2 changes: 2 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,45 @@ class TombstoneIntegrationTest : ApplicationExitIntegrationTestBase<TombstoneHin
assertEquals(57344, image.imageSize)
}

@Test
fun `when attachRawTombstone is enabled, raw tombstone is attached to hint`() {
val integration =
fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp) { options ->
options.isAttachRawTombstone = true
}

fixture.addAppExitInfo(timestamp = newTimestamp)

integration.register(fixture.scopes, fixture.options)

verify(fixture.scopes)
.captureEvent(
any(),
argThat<Hint> {
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<Hint> { this.tombstone == null })
}

@Test
fun `when matching native event has attachments, they are added to the hint`() {
val integration =
Expand Down
3 changes: 3 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -613,13 +614,15 @@ 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
public fun set (Ljava/lang/String;Ljava/lang/Object;)V
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;
Expand Down
10 changes: 10 additions & 0 deletions sentry/src/main/java/io/sentry/Attachment.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
9 changes: 9 additions & 0 deletions sentry/src/main/java/io/sentry/Hint.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions sentry/src/main/java/io/sentry/SentryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,11 @@ private boolean shouldSendSessionUpdateForDroppedEvent(
attachments.add(threadDump);
}

@Nullable final Attachment tombstone = hint.getTombstone();
if (tombstone != null) {
attachments.add(tombstone);
}

return attachments;
}

Expand Down
Loading