Skip to content

Commit ba0c7db

Browse files
committed
feat: minimal tombstone integration (disabled by default, options internal)
1 parent 8285e52 commit ba0c7db

File tree

9 files changed

+751
-0
lines changed

9 files changed

+751
-0
lines changed

gradle/libs.versions.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ spotless = "7.0.4"
4141
gummyBears = "0.12.0"
4242
camerax = "1.3.0"
4343
openfeature = "1.18.2"
44+
protobuf = "4.33.1"
4445

4546
[plugins]
4647
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
@@ -60,6 +61,7 @@ spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
6061
detekt = { id = "io.gitlab.arturbosch.detekt", version = "1.23.8" }
6162
jacoco-android = { id = "com.mxalbert.gradle.jacoco-android", version = "0.2.0" }
6263
kover = { id = "org.jetbrains.kotlinx.kover", version = "0.7.3" }
64+
protobuf = { id = "com.google.protobuf", version = "0.9.5" }
6365
vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version = "0.30.0" }
6466
springboot2 = { id = "org.springframework.boot", version.ref = "springboot2" }
6567
springboot3 = { id = "org.springframework.boot", version.ref = "springboot3" }
@@ -138,6 +140,8 @@ otel-javaagent-extension-api = { module = "io.opentelemetry.javaagent:openteleme
138140
otel-semconv = { module = "io.opentelemetry.semconv:opentelemetry-semconv", version.ref = "otelSemanticConventions" }
139141
otel-semconv-incubating = { module = "io.opentelemetry.semconv:opentelemetry-semconv-incubating", version.ref = "otelSemanticConventionsAlpha" }
140142
p6spy = { module = "p6spy:p6spy", version = "3.9.1" }
143+
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf"}
144+
protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
141145
quartz = { module = "org.quartz-scheduler:quartz", version = "2.3.0" }
142146
reactor-core = { module = "io.projectreactor:reactor-core", version = "3.5.3" }
143147
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }

sentry-android-core/build.gradle.kts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ plugins {
88
alias(libs.plugins.jacoco.android)
99
alias(libs.plugins.errorprone)
1010
alias(libs.plugins.gradle.versions)
11+
alias(libs.plugins.protobuf)
1112
}
1213

1314
android {
@@ -83,6 +84,7 @@ dependencies {
8384
implementation(libs.androidx.lifecycle.common.java8)
8485
implementation(libs.androidx.lifecycle.process)
8586
implementation(libs.androidx.core)
87+
implementation(libs.protobuf.javalite)
8688

8789
errorprone(libs.errorprone.core)
8890
errorprone(libs.nopen.checker)
@@ -109,3 +111,18 @@ dependencies {
109111
testRuntimeOnly(libs.androidx.fragment.ktx)
110112
testRuntimeOnly(libs.timber)
111113
}
114+
115+
protobuf {
116+
protoc {
117+
artifact = libs.protoc.get().toString()
118+
}
119+
generateProtoTasks {
120+
all().forEach { task ->
121+
task.builtins {
122+
create("java") {
123+
option("lite")
124+
}
125+
}
126+
}
127+
}
128+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import android.app.Application;
66
import android.content.Context;
77
import android.content.pm.PackageInfo;
8+
import android.os.Build;
89
import io.sentry.CompositePerformanceCollector;
910
import io.sentry.DeduplicateMultithreadedEventProcessor;
1011
import io.sentry.DefaultCompositePerformanceCollector;
@@ -372,6 +373,10 @@ static void installDefaultIntegrations(
372373
final Class<?> sentryNdkClass = loadClass.loadClass(SENTRY_NDK_CLASS_NAME, options.getLogger());
373374
options.addIntegration(new NdkIntegration(sentryNdkClass));
374375

376+
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.R) {
377+
options.addIntegration(new TombstoneIntegration(context));
378+
}
379+
375380
// this integration uses android.os.FileObserver, we can't move to sentry
376381
// before creating a pure java impl.
377382
options.addIntegration(EnvelopeFileObserverIntegration.getOutboxFileObserver());

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ public interface BeforeCaptureCallback {
227227

228228
private @Nullable SentryFrameMetricsCollector frameMetricsCollector;
229229

230+
private boolean tombstonesEnabled = false;
231+
230232
public SentryAndroidOptions() {
231233
setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
232234
setSdkVersion(createSdkVersion());
@@ -300,6 +302,26 @@ public void setAnrReportInDebug(boolean anrReportInDebug) {
300302
this.anrReportInDebug = anrReportInDebug;
301303
}
302304

305+
/**
306+
* Sets Tombstone reporting (ApplicationExitInfo.REASON_CRASH_NATIVE) to enabled or disabled.
307+
*
308+
* @param tombstonesEnabled true for enabled and false for disabled
309+
*/
310+
@ApiStatus.Internal
311+
public void setTombstonesEnabled(boolean tombstonesEnabled) {
312+
this.tombstonesEnabled = tombstonesEnabled;
313+
}
314+
315+
/**
316+
* Checks if Tombstone reporting (ApplicationExitInfo.REASON_CRASH_NATIVE) is enabled or disabled Default is disabled
317+
*
318+
* @return true if enabled or false otherwise
319+
*/
320+
@ApiStatus.Internal
321+
public boolean isTombstonesEnabled() {
322+
return tombstonesEnabled;
323+
}
324+
303325
public boolean isEnableActivityLifecycleBreadcrumbs() {
304326
return enableActivityLifecycleBreadcrumbs;
305327
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package io.sentry.android.core;
2+
3+
import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;
4+
5+
import android.app.ActivityManager;
6+
import android.app.ApplicationExitInfo;
7+
import android.content.Context;
8+
import android.os.Build;
9+
10+
import androidx.annotation.RequiresApi;
11+
12+
import io.sentry.DateUtils;
13+
import io.sentry.IScopes;
14+
import io.sentry.Integration;
15+
import io.sentry.SentryEvent;
16+
import io.sentry.SentryLevel;
17+
import io.sentry.SentryOptions;
18+
import io.sentry.android.core.internal.tombstone.TombstoneParser;
19+
import io.sentry.cache.EnvelopeCache;
20+
import io.sentry.cache.IEnvelopeCache;
21+
import io.sentry.transport.CurrentDateProvider;
22+
import io.sentry.transport.ICurrentDateProvider;
23+
import io.sentry.util.Objects;
24+
25+
import java.io.Closeable;
26+
import java.io.IOException;
27+
import java.util.ArrayList;
28+
import java.util.List;
29+
import java.util.concurrent.TimeUnit;
30+
31+
import org.jetbrains.annotations.NotNull;
32+
import org.jetbrains.annotations.Nullable;
33+
34+
public class TombstoneIntegration implements Integration, Closeable {
35+
static final long NINETY_DAYS_THRESHOLD = TimeUnit.DAYS.toMillis(91);
36+
37+
private final @NotNull Context context;
38+
private final @NotNull ICurrentDateProvider dateProvider;
39+
private @Nullable SentryAndroidOptions options;
40+
41+
public TombstoneIntegration(final @NotNull Context context) {
42+
// using CurrentDateProvider instead of AndroidCurrentDateProvider as AppExitInfo uses
43+
// System.currentTimeMillis
44+
this(context, CurrentDateProvider.getInstance());
45+
}
46+
47+
TombstoneIntegration(
48+
final @NotNull Context context, final @NotNull ICurrentDateProvider dateProvider) {
49+
this.context = ContextUtils.getApplicationContext(context);
50+
this.dateProvider = dateProvider;
51+
}
52+
53+
@Override
54+
public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) {
55+
this.options =
56+
Objects.requireNonNull(
57+
(options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null,
58+
"SentryAndroidOptions is required");
59+
60+
this.options
61+
.getLogger()
62+
.log(SentryLevel.DEBUG, "TombstoneIntegration enabled: %s", this.options.isTombstonesEnabled());
63+
64+
if (this.options.isTombstonesEnabled()) {
65+
if (this.options.getCacheDirPath() == null) {
66+
this.options
67+
.getLogger()
68+
.log(SentryLevel.INFO, "Cache dir is not set, unable to process Tombstones");
69+
return;
70+
}
71+
72+
try {
73+
options
74+
.getExecutorService()
75+
.submit(
76+
new TombstoneProcessor(
77+
context, scopes, this.options, dateProvider));
78+
} catch (Throwable e) {
79+
options.getLogger().log(SentryLevel.DEBUG, "Failed to start TombstoneProcessor.", e);
80+
}
81+
options.getLogger().log(SentryLevel.DEBUG, "TombstoneIntegration installed.");
82+
addIntegrationToSdkVersion("Tombstone");
83+
}
84+
}
85+
86+
@Override
87+
public void close() throws IOException {
88+
if (options != null) {
89+
options.getLogger().log(SentryLevel.DEBUG, "TombstoneIntegration removed.");
90+
}
91+
}
92+
93+
public static class TombstoneProcessor implements Runnable {
94+
95+
@NotNull
96+
private final Context context;
97+
@NotNull
98+
private final IScopes scopes;
99+
@NotNull
100+
private final SentryAndroidOptions options;
101+
private final long threshold;
102+
103+
public TombstoneProcessor(
104+
@NotNull Context context,
105+
@NotNull IScopes scopes,
106+
@NotNull SentryAndroidOptions options,
107+
@NotNull ICurrentDateProvider dateProvider) {
108+
this.context = context;
109+
this.scopes = scopes;
110+
this.options = options;
111+
112+
this.threshold = dateProvider.getCurrentTimeMillis() - NINETY_DAYS_THRESHOLD;
113+
}
114+
115+
@Override
116+
@RequiresApi(api = Build.VERSION_CODES.R)
117+
public void run() {
118+
final ActivityManager activityManager =
119+
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
120+
121+
final List<ApplicationExitInfo> applicationExitInfoList;
122+
applicationExitInfoList = activityManager.getHistoricalProcessExitReasons(null, 0, 0);
123+
124+
if (applicationExitInfoList.isEmpty()) {
125+
options.getLogger().log(SentryLevel.DEBUG, "No records in historical exit reasons.");
126+
return;
127+
}
128+
129+
final IEnvelopeCache cache = options.getEnvelopeDiskCache();
130+
if (cache instanceof EnvelopeCache) {
131+
if (options.isEnableAutoSessionTracking()
132+
&& !((EnvelopeCache) cache).waitPreviousSessionFlush()) {
133+
options
134+
.getLogger()
135+
.log(
136+
SentryLevel.WARNING,
137+
"Timed out waiting to flush previous session to its own file.");
138+
139+
// if we timed out waiting here, we can already flush the latch, because the timeout is
140+
// big
141+
// enough to wait for it only once and we don't have to wait again in
142+
// PreviousSessionFinalizer
143+
((EnvelopeCache) cache).flushPreviousSession();
144+
}
145+
}
146+
147+
// making a deep copy as we're modifying the list
148+
final List<ApplicationExitInfo> exitInfos = new ArrayList<>(applicationExitInfoList);
149+
150+
// search for the latest Tombstone to report it separately as we're gonna enrich it. The
151+
// latest
152+
// Tombstone will be first in the list, as it's filled last-to-first in order of appearance
153+
ApplicationExitInfo latestTombstone = null;
154+
for (ApplicationExitInfo applicationExitInfo : exitInfos) {
155+
if (applicationExitInfo.getReason() == ApplicationExitInfo.REASON_CRASH_NATIVE) {
156+
latestTombstone = applicationExitInfo;
157+
// remove it, so it's not reported twice
158+
// TODO: if we fail after this, we effectively lost the ApplicationExitInfo (maybe only remove after we reported it)
159+
exitInfos.remove(applicationExitInfo);
160+
break;
161+
}
162+
}
163+
164+
if (latestTombstone == null) {
165+
options
166+
.getLogger()
167+
.log(
168+
SentryLevel.DEBUG,
169+
"No Tombstones have been found in the historical exit reasons list.");
170+
return;
171+
}
172+
173+
if (latestTombstone.getTimestamp() < threshold) {
174+
options
175+
.getLogger()
176+
.log(SentryLevel.DEBUG, "Latest Tombstones happened too long ago, returning early.");
177+
return;
178+
}
179+
180+
reportAsSentryEvent(latestTombstone);
181+
}
182+
183+
@RequiresApi(api = Build.VERSION_CODES.R)
184+
private void reportAsSentryEvent(ApplicationExitInfo exitInfo) {
185+
SentryEvent event;
186+
try {
187+
TombstoneParser parser = new TombstoneParser(exitInfo.getTraceInputStream());
188+
event = parser.parse();
189+
event.setTimestamp(DateUtils.getDateTime(exitInfo.getTimestamp()));
190+
} catch (IOException e) {
191+
throw new RuntimeException(e);
192+
}
193+
194+
scopes.captureEvent(event);
195+
}
196+
}
197+
}

0 commit comments

Comments
 (0)