Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ba0c7db
feat: minimal tombstone integration (disabled by default, options int…
supervacuus Nov 25, 2025
9933e47
redo a seemingly missed spotlessApply
supervacuus Nov 26, 2025
10b1e94
use non-plural option + mark integration also as internal + update API
supervacuus Nov 26, 2025
f12b658
Merge branch 'main' into feat/tombstone_integration
supervacuus Nov 26, 2025
4b6e1fb
init size exceptions since we know we only need one
supervacuus Nov 27, 2025
95bd726
move `instructionAddressAdjustment` to `SentryStackTrace`
supervacuus Dec 2, 2025
4f03857
add copyright notice to tombstone.proto
supervacuus Dec 2, 2025
936a2f0
add historical tombstone option
supervacuus Dec 2, 2025
28652e0
looks like we need to add a TombstoneEventProcessor anyway.
supervacuus Dec 2, 2025
920221a
...so let's add it to the options if the SDK supports it (adapt to >=…
supervacuus Dec 2, 2025
48ed5e1
Adapt `AndroidEnvelopeCache` to also write Tombstone timestamp markers
supervacuus Dec 2, 2025
10c7a1f
Integrate handling of historical Tombstone option + last tombstone ma…
supervacuus Dec 2, 2025
6cbfdf8
sprinkle with TODOs to highlight next PR steps
supervacuus Dec 2, 2025
4b1919e
fix typo
supervacuus Dec 2, 2025
d931ddc
implement TombstoneParser as Closable and close the tombstone stream
supervacuus Dec 2, 2025
65bc76b
tighten code with final and null annotations.
supervacuus Dec 2, 2025
25f4089
eliminate obsolete null check
supervacuus Dec 2, 2025
7db5895
Deduplicate ApplicationExitInfo handling for corresponding Integratio…
supervacuus Dec 3, 2025
a8ea230
update tombstone message construction
supervacuus Dec 3, 2025
92cb264
Merge branch 'main' into feat/tombstone_integration
supervacuus Dec 3, 2025
ed81771
fix abortMessage check
supervacuus Dec 3, 2025
480bfd6
Merge branch 'main' into feat/tombstone_integration
supervacuus Dec 3, 2025
c8acdf9
Merge branch 'main' into feat/tombstone_integration
supervacuus Dec 3, 2025
125b0c4
convert AnrV2EventProcessor to a more generic ApplicationExitInfoEven…
supervacuus Dec 3, 2025
7e29fbf
reintroduce, update and correct old inline docs where they make sense.
supervacuus Dec 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ spotless = "7.0.4"
gummyBears = "0.12.0"
camerax = "1.3.0"
openfeature = "1.18.2"
protobuf = "4.33.1"

[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
Expand All @@ -60,6 +61,7 @@ spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
detekt = { id = "io.gitlab.arturbosch.detekt", version = "1.23.8" }
jacoco-android = { id = "com.mxalbert.gradle.jacoco-android", version = "0.2.0" }
kover = { id = "org.jetbrains.kotlinx.kover", version = "0.7.3" }
protobuf = { id = "com.google.protobuf", version = "0.9.5" }
vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version = "0.30.0" }
springboot2 = { id = "org.springframework.boot", version.ref = "springboot2" }
springboot3 = { id = "org.springframework.boot", version.ref = "springboot3" }
Expand Down Expand Up @@ -138,6 +140,8 @@ otel-javaagent-extension-api = { module = "io.opentelemetry.javaagent:openteleme
otel-semconv = { module = "io.opentelemetry.semconv:opentelemetry-semconv", version.ref = "otelSemanticConventions" }
otel-semconv-incubating = { module = "io.opentelemetry.semconv:opentelemetry-semconv-incubating", version.ref = "otelSemanticConventionsAlpha" }
p6spy = { module = "p6spy:p6spy", version = "3.9.1" }
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@romtsn Not sure if you followed the conversations, but as you can see here, protobuf requires a runtime dependency. It will have an impact of around 10kb. IMHO fine for now, we should still check how stable this library is to avoid and consumer version mismatch issues.

protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
quartz = { module = "org.quartz-scheduler:quartz", version = "2.3.0" }
reactor-core = { module = "io.projectreactor:reactor-core", version = "3.5.3" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
Expand Down
19 changes: 19 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun isEnableSystemEventBreadcrumbs ()Z
public fun isEnableSystemEventBreadcrumbsExtras ()Z
public fun isReportHistoricalAnrs ()Z
public fun isTombstoneEnabled ()Z
public fun setAnrEnabled (Z)V
public fun setAnrReportInDebug (Z)V
public fun setAnrTimeoutIntervalMillis (J)V
Expand Down Expand Up @@ -367,6 +368,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun setNativeHandlerStrategy (Lio/sentry/android/core/NdkHandlerStrategy;)V
public fun setNativeSdkName (Ljava/lang/String;)V
public fun setReportHistoricalAnrs (Z)V
public fun setTombstoneEnabled (Z)V
}

public abstract interface class io/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback {
Expand Down Expand Up @@ -455,6 +457,21 @@ public final class io/sentry/android/core/SystemEventsBreadcrumbsIntegration : i
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
}

public class io/sentry/android/core/TombstoneEventProcessor : io/sentry/BackfillingEventProcessor {
public fun <init> (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V
}

public class io/sentry/android/core/TombstoneIntegration : io/sentry/Integration, java/io/Closeable {
public fun <init> (Landroid/content/Context;)V
public fun close ()V
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
}

public class io/sentry/android/core/TombstoneIntegration$TombstoneProcessor : java/lang/Runnable {
public fun <init> (Landroid/content/Context;Lio/sentry/IScopes;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/transport/ICurrentDateProvider;)V
public fun run ()V
}

public final class io/sentry/android/core/UserInteractionIntegration : android/app/Application$ActivityLifecycleCallbacks, io/sentry/Integration, java/io/Closeable {
public fun <init> (Landroid/app/Application;Lio/sentry/util/LoadClass;)V
public fun close ()V
Expand Down Expand Up @@ -482,10 +499,12 @@ public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentr

public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry/cache/EnvelopeCache {
public static final field LAST_ANR_REPORT Ljava/lang/String;
public static final field LAST_TOMBSTONE_REPORT Ljava/lang/String;
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;)V
public fun getDirectory ()Ljava/io/File;
public static fun hasStartupCrashMarker (Lio/sentry/SentryOptions;)Z
public static fun lastReportedAnr (Lio/sentry/SentryOptions;)Ljava/lang/Long;
public static fun lastReportedTombstone (Lio/sentry/SentryOptions;)Ljava/lang/Long;
public fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V
public fun storeEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Z
}
Expand Down
9 changes: 9 additions & 0 deletions sentry-android-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ plugins {
alias(libs.plugins.jacoco.android)
alias(libs.plugins.errorprone)
alias(libs.plugins.gradle.versions)
alias(libs.plugins.protobuf)
}

android {
Expand Down Expand Up @@ -83,6 +84,7 @@ dependencies {
implementation(libs.androidx.lifecycle.common.java8)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.core)
implementation(libs.protobuf.javalite)

errorprone(libs.errorprone.core)
errorprone(libs.nopen.checker)
Expand All @@ -109,3 +111,10 @@ dependencies {
testRuntimeOnly(libs.androidx.fragment.ktx)
testRuntimeOnly(libs.timber)
}

protobuf {
protoc { artifact = libs.protoc.get().toString() }
generateProtoTasks {
all().forEach { task -> task.builtins { create("java") { option("lite") } } }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import android.app.Application;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.os.Build;
import io.sentry.CompositePerformanceCollector;
import io.sentry.DeduplicateMultithreadedEventProcessor;
import io.sentry.DefaultCompositePerformanceCollector;
Expand Down Expand Up @@ -188,6 +189,9 @@ static void initializeIntegrationsAndProcessors(
options.addEventProcessor(new ScreenshotEventProcessor(options, buildInfoProvider));
options.addEventProcessor(new ViewHierarchyEventProcessor(options));
options.addEventProcessor(new AnrV2EventProcessor(context, options, buildInfoProvider));
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.S) {
options.addEventProcessor(new TombstoneEventProcessor(context, options, buildInfoProvider));
}
if (options.getTransportGate() instanceof NoOpTransportGate) {
options.setTransportGate(new AndroidTransportGate(options));
}
Expand Down Expand Up @@ -372,6 +376,10 @@ static void installDefaultIntegrations(
final Class<?> sentryNdkClass = loadClass.loadClass(SENTRY_NDK_CLASS_NAME, options.getLogger());
options.addIntegration(new NdkIntegration(sentryNdkClass));

if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.S) {
options.addIntegration(new TombstoneIntegration(context));
}

// this integration uses android.os.FileObserver, we can't move to sentry
// before creating a pure java impl.
options.addIntegration(EnvelopeFileObserverIntegration.getOutboxFileObserver());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ public AnrV2EventProcessor(
return event;
}

// TODO: right now, any event with a Backfillable Hint will get here. This is fine if the
// enrichment code is generically applicable to any Backfilling Event. However some parts
// in the ANRv2EventProcessor are specific to ANRs and currently override the tombstone
// events. Similar to the Integration we should find an abstraction split that allows to
// separate the generic from the specific.

// we always set exception values, platform, os and device even if the ANR is not enrich-able
// even though the OS context may change in the meantime (OS update), we consider this an
// edge-case
Expand Down Expand Up @@ -255,6 +261,7 @@ private void setFingerprints(final @NotNull SentryEvent event, final @NotNull Ob
// sentry does not yet have a capability to provide default server-side fingerprint rules,
// so we're doing this on the SDK side to group background and foreground ANRs separately
// even if they have similar stacktraces
// TODO: this will always set the fingerprint of tombstones to foreground-anr
final boolean isBackgroundAnr = isBackgroundAnr(hint);
if (event.getFingerprints() == null) {
event.setFingerprints(
Expand Down Expand Up @@ -385,6 +392,8 @@ private void setApp(final @NotNull SentryBaseEvent event, final @NotNull Object
// TODO: not entirely correct, because we define background ANRs as not the ones of
// IMPORTANCE_FOREGROUND, but this doesn't mean the app was in foreground when an ANR happened
// but it's our best effort for now. We could serialize AppState in theory.

// TODO: this will always be true of tombstones
app.setInForeground(!isBackgroundAnr(hint));

final PackageInfo packageInfo = ContextUtils.getPackageInfo(context, buildInfoProvider);
Expand Down Expand Up @@ -533,6 +542,9 @@ private void setStaticValues(final @NotNull SentryEvent event) {
private void setPlatform(final @NotNull SentryBaseEvent event) {
if (event.getPlatform() == null) {
// this actually means JVM related.
// TODO: since we write this from the tombstone parser we are current unaffected. It is good
// that it doesn't overwrite previous platform values, however we still rely on the
// order in which each event processor was called.
event.setPlatform(SentryBaseEvent.DEFAULT_PLATFORM);
}
}
Expand Down Expand Up @@ -564,12 +576,16 @@ private void setExceptions(final @NotNull SentryEvent event, final @NotNull Obje
// and make an exception out of its stacktrace
final Mechanism mechanism = new Mechanism();
if (!((Backfillable) hint).shouldEnrich()) {
// TODO: this currently overrides the signalhandler mechanism we set in the TombstoneParser
// with a new type (can be the right choice down the road, but currently is
// unintentional, and it might be better to leave it close to the Native SDK)
// we only enrich the latest ANR in the list, so this is historical
mechanism.setType("HistoricalAppExitInfo");
} else {
mechanism.setType("AppExitInfo");
}

// TODO: this currently overrides the tombstone exceptions
final boolean isBackgroundAnr = isBackgroundAnr(hint);
String message = "ANR";
if (isBackgroundAnr) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public final class SentryAndroidOptions extends SentryOptions {
* <li>The transaction status will be {@link SpanStatus#OK} if none is set.
* </ul>
*
* The transaction is automatically bound to the {@link IScope}, but only if there's no
* <p>The transaction is automatically bound to the {@link IScope}, but only if there's no
* transaction already bound to the Scope.
*/
private boolean enableAutoActivityLifecycleTracing = true;
Expand Down Expand Up @@ -217,6 +217,17 @@ public interface BeforeCaptureCallback {
*/
private boolean reportHistoricalAnrs = false;

/**
* Controls whether to report historical Tombstones from the {@link ApplicationExitInfo} system
* API. When enabled, reports all of the Tombstones available in the {@link
* ActivityManager#getHistoricalProcessExitReasons(String, int, int)} list, as opposed to
* reporting only the latest one.
*
* <p>These events do not affect crash rate nor are they enriched with additional information from
* {@link IScope} like breadcrumbs.
*/
private boolean reportHistoricalTombstones = 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.
Expand All @@ -227,6 +238,8 @@ public interface BeforeCaptureCallback {

private @Nullable SentryFrameMetricsCollector frameMetricsCollector;

private boolean enableTombstone = false;

public SentryAndroidOptions() {
setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
setSdkVersion(createSdkVersion());
Expand Down Expand Up @@ -300,6 +313,27 @@ public void setAnrReportInDebug(boolean anrReportInDebug) {
this.anrReportInDebug = anrReportInDebug;
}

/**
* Sets Tombstone reporting (ApplicationExitInfo.REASON_CRASH_NATIVE) to enabled or disabled.
*
* @param enableTombstone true for enabled and false for disabled
*/
@ApiStatus.Internal
public void setTombstoneEnabled(boolean enableTombstone) {
this.enableTombstone = enableTombstone;
}

/**
* Checks if Tombstone reporting (ApplicationExitInfo.REASON_CRASH_NATIVE) is enabled or disabled
* Default is disabled
*
* @return true if enabled or false otherwise
*/
@ApiStatus.Internal
public boolean isTombstoneEnabled() {
return enableTombstone;
}

public boolean isEnableActivityLifecycleBreadcrumbs() {
return enableActivityLifecycleBreadcrumbs;
}
Expand Down Expand Up @@ -570,6 +604,14 @@ public void setReportHistoricalAnrs(final boolean reportHistoricalAnrs) {
this.reportHistoricalAnrs = reportHistoricalAnrs;
}

public boolean isReportHistoricalTombstones() {
return reportHistoricalTombstones;
}

public void setReportHistoricalTombstones(final boolean reportHistoricalTombstones) {
this.reportHistoricalTombstones = reportHistoricalTombstones;
}

public boolean isAttachAnrThreadDump() {
return attachAnrThreadDump;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.sentry.android.core;

import android.content.Context;
import androidx.annotation.WorkerThread;
import io.sentry.BackfillingEventProcessor;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/** originating from the Native SDK. */
@ApiStatus.Internal
@WorkerThread
public class TombstoneEventProcessor implements BackfillingEventProcessor {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can get rid of this then in favor of ApplicationExitInfoEventProcessor and the hint based enrichers in there.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is an obsolete leftover of my initial attempt. However, once we connect the Native SDK with tombstones, a separate EventProcessor could make sense again.


@NotNull private final Context context;
@NotNull private final SentryAndroidOptions options;
@NotNull private final BuildInfoProvider buildInfoProvider;

public TombstoneEventProcessor(
@NotNull Context context,
@NotNull SentryAndroidOptions options,
@NotNull BuildInfoProvider buildInfoProvider) {
this.context = context;
this.options = options;
this.buildInfoProvider = buildInfoProvider;
}
}
Loading
Loading