Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 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
605a840
remove obsolete TombstoneEventProcessor
supervacuus Dec 5, 2025
76ef13c
Merge branch 'main' into feat/tombstone_integration
supervacuus Dec 5, 2025
7f6dbc9
clean up tombstone error handling
supervacuus Dec 5, 2025
58a342f
convert ApplicationExitInfoHistoryDispatcher.removeLatest() to use an…
supervacuus Dec 5, 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
17 changes: 17 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,18 @@ 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 @@ -372,6 +373,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.R) {
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 @@ -227,6 +227,8 @@ public interface BeforeCaptureCallback {

private @Nullable SentryFrameMetricsCollector frameMetricsCollector;

private boolean tombstonesEnabled = false;

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

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

/**
* 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 isTombstonesEnabled() {
return tombstonesEnabled;
}

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

import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;

import android.app.ActivityManager;
import android.app.ApplicationExitInfo;
import android.content.Context;
import android.os.Build;

import androidx.annotation.RequiresApi;

import io.sentry.DateUtils;
import io.sentry.IScopes;
import io.sentry.Integration;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.android.core.internal.tombstone.TombstoneParser;
import io.sentry.cache.EnvelopeCache;
import io.sentry.cache.IEnvelopeCache;
import io.sentry.transport.CurrentDateProvider;
import io.sentry.transport.ICurrentDateProvider;
import io.sentry.util.Objects;

import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class TombstoneIntegration implements Integration, Closeable {
static final long NINETY_DAYS_THRESHOLD = TimeUnit.DAYS.toMillis(91);

private final @NotNull Context context;
private final @NotNull ICurrentDateProvider dateProvider;
private @Nullable SentryAndroidOptions options;

public TombstoneIntegration(final @NotNull Context context) {
// using CurrentDateProvider instead of AndroidCurrentDateProvider as AppExitInfo uses
// System.currentTimeMillis
this(context, CurrentDateProvider.getInstance());
}

TombstoneIntegration(
final @NotNull Context context, final @NotNull ICurrentDateProvider dateProvider) {
this.context = ContextUtils.getApplicationContext(context);
this.dateProvider = dateProvider;
}

@Override
public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) {
this.options =
Objects.requireNonNull(
(options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null,
"SentryAndroidOptions is required");

this.options
.getLogger()
.log(SentryLevel.DEBUG, "TombstoneIntegration enabled: %s", this.options.isTombstonesEnabled());

if (this.options.isTombstonesEnabled()) {
if (this.options.getCacheDirPath() == null) {
this.options
.getLogger()
.log(SentryLevel.INFO, "Cache dir is not set, unable to process Tombstones");
return;
}

try {
options
.getExecutorService()
.submit(
new TombstoneProcessor(
context, scopes, this.options, dateProvider));
} catch (Throwable e) {
options.getLogger().log(SentryLevel.DEBUG, "Failed to start TombstoneProcessor.", e);
}
options.getLogger().log(SentryLevel.DEBUG, "TombstoneIntegration installed.");
addIntegrationToSdkVersion("Tombstone");
}
}

@Override
public void close() throws IOException {
if (options != null) {
options.getLogger().log(SentryLevel.DEBUG, "TombstoneIntegration removed.");
}
}

public static class TombstoneProcessor implements Runnable {

@NotNull
private final Context context;
@NotNull
private final IScopes scopes;
@NotNull
private final SentryAndroidOptions options;
private final long threshold;

public TombstoneProcessor(
@NotNull Context context,
@NotNull IScopes scopes,
@NotNull SentryAndroidOptions options,
@NotNull ICurrentDateProvider dateProvider) {
this.context = context;
this.scopes = scopes;
this.options = options;

this.threshold = dateProvider.getCurrentTimeMillis() - NINETY_DAYS_THRESHOLD;
}

@Override
@RequiresApi(api = Build.VERSION_CODES.R)
public void run() {
final ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

final List<ApplicationExitInfo> applicationExitInfoList;
applicationExitInfoList = activityManager.getHistoricalProcessExitReasons(null, 0, 0);

if (applicationExitInfoList.isEmpty()) {
options.getLogger().log(SentryLevel.DEBUG, "No records in historical exit reasons.");
return;
}

final IEnvelopeCache cache = options.getEnvelopeDiskCache();
if (cache instanceof EnvelopeCache) {
if (options.isEnableAutoSessionTracking()
&& !((EnvelopeCache) cache).waitPreviousSessionFlush()) {
options
.getLogger()
.log(
SentryLevel.WARNING,
"Timed out waiting to flush previous session to its own file.");

// if we timed out waiting here, we can already flush the latch, because the timeout is
// big
// enough to wait for it only once and we don't have to wait again in
// PreviousSessionFinalizer
((EnvelopeCache) cache).flushPreviousSession();
}
}

// making a deep copy as we're modifying the list
final List<ApplicationExitInfo> exitInfos = new ArrayList<>(applicationExitInfoList);

// search for the latest Tombstone to report it separately as we're gonna enrich it. The
// latest
// Tombstone will be first in the list, as it's filled last-to-first in order of appearance
ApplicationExitInfo latestTombstone = null;
for (ApplicationExitInfo applicationExitInfo : exitInfos) {
if (applicationExitInfo.getReason() == ApplicationExitInfo.REASON_CRASH_NATIVE) {
latestTombstone = applicationExitInfo;
// remove it, so it's not reported twice
// TODO: if we fail after this, we effectively lost the ApplicationExitInfo (maybe only remove after we reported it)
exitInfos.remove(applicationExitInfo);
break;
}
}

if (latestTombstone == null) {
options
.getLogger()
.log(
SentryLevel.DEBUG,
"No Tombstones have been found in the historical exit reasons list.");
return;
}

if (latestTombstone.getTimestamp() < threshold) {
options
.getLogger()
.log(SentryLevel.DEBUG, "Latest Tombstones happened too long ago, returning early.");
return;
}

reportAsSentryEvent(latestTombstone);
}

@RequiresApi(api = Build.VERSION_CODES.R)
private void reportAsSentryEvent(ApplicationExitInfo exitInfo) {
SentryEvent event;
try {
TombstoneParser parser = new TombstoneParser(exitInfo.getTraceInputStream());
event = parser.parse();
event.setTimestamp(DateUtils.getDateTime(exitInfo.getTimestamp()));
} catch (IOException e) {
throw new RuntimeException(e);
}

scopes.captureEvent(event);
}
}
}
Loading
Loading