Skip to content

Commit 10c7a1f

Browse files
committed
Integrate handling of historical Tombstone option + last tombstone marker
This currently does not work: While we now can optionally enable reporting of "historical" tombstones, by making the `TombstoneHint` `Backfillable` it will automatically be enriched by the `ANRv2EventProcessor` which is currently the only `BackfillingEventProcessor`. The `ANRv2EventProcessor` is partially written in way that is potentially generic for events with `Backfillable` hints, but other parts are enriching as if those are always were ANRs, which up to now was true, but with Tombstones that assumption now breaks. Next Steps: * There is considerable duplication between the ANRv2Integration and TombstoneIntegration
1 parent 48ed5e1 commit 10c7a1f

File tree

2 files changed

+168
-11
lines changed

2 files changed

+168
-11
lines changed

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

Lines changed: 153 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,32 @@
88
import android.os.Build;
99
import androidx.annotation.RequiresApi;
1010
import io.sentry.DateUtils;
11+
import io.sentry.Hint;
12+
import io.sentry.ILogger;
1113
import io.sentry.IScopes;
1214
import io.sentry.Integration;
1315
import io.sentry.SentryEvent;
1416
import io.sentry.SentryLevel;
1517
import io.sentry.SentryOptions;
18+
import io.sentry.android.core.cache.AndroidEnvelopeCache;
1619
import io.sentry.android.core.internal.tombstone.TombstoneParser;
1720
import io.sentry.cache.EnvelopeCache;
1821
import io.sentry.cache.IEnvelopeCache;
22+
import io.sentry.hints.Backfillable;
23+
import io.sentry.hints.BlockingFlushHint;
24+
import io.sentry.hints.NativeCrashExit;
25+
import io.sentry.protocol.SentryId;
1926
import io.sentry.transport.CurrentDateProvider;
2027
import io.sentry.transport.ICurrentDateProvider;
28+
import io.sentry.util.HintUtils;
2129
import io.sentry.util.Objects;
2230
import java.io.Closeable;
2331
import java.io.IOException;
32+
import java.io.InputStream;
33+
import java.time.Instant;
34+
import java.time.format.DateTimeFormatter;
2435
import java.util.ArrayList;
36+
import java.util.Collections;
2537
import java.util.List;
2638
import java.util.concurrent.TimeUnit;
2739
import org.jetbrains.annotations.ApiStatus;
@@ -115,6 +127,11 @@ public void run() {
115127
final ActivityManager activityManager =
116128
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
117129

130+
if (activityManager == null) {
131+
options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve ActivityManager.");
132+
return;
133+
}
134+
118135
final List<ApplicationExitInfo> applicationExitInfoList;
119136
applicationExitInfoList = activityManager.getHistoricalProcessExitReasons(null, 0, 0);
120137

@@ -134,15 +151,16 @@ public void run() {
134151
"Timed out waiting to flush previous session to its own file.");
135152

136153
// if we timed out waiting here, we can already flush the latch, because the timeout is
137-
// big
138-
// enough to wait for it only once and we don't have to wait again in
154+
// big enough to wait for it only once and we don't have to wait again in
139155
// PreviousSessionFinalizer
140156
((EnvelopeCache) cache).flushPreviousSession();
141157
}
142158
}
143159

144160
// making a deep copy as we're modifying the list
145161
final List<ApplicationExitInfo> exitInfos = new ArrayList<>(applicationExitInfoList);
162+
final @Nullable Long lastReportedTombstoneTimestamp =
163+
AndroidEnvelopeCache.lastReportedTombstone(options);
146164

147165
// search for the latest Tombstone to report it separately as we're gonna enrich it. The
148166
// latest
@@ -152,8 +170,6 @@ public void run() {
152170
if (applicationExitInfo.getReason() == ApplicationExitInfo.REASON_CRASH_NATIVE) {
153171
latestTombstone = applicationExitInfo;
154172
// remove it, so it's not reported twice
155-
// TODO: if we fail after this, we effectively lost the ApplicationExitInfo (maybe only
156-
// remove after we reported it)
157173
exitInfos.remove(applicationExitInfo);
158174
break;
159175
}
@@ -171,25 +187,151 @@ public void run() {
171187
if (latestTombstone.getTimestamp() < threshold) {
172188
options
173189
.getLogger()
174-
.log(SentryLevel.DEBUG, "Latest Tombstones happened too long ago, returning early.");
190+
.log(SentryLevel.DEBUG, "Latest Tombstone happened too long ago, returning early.");
191+
return;
192+
}
193+
194+
if (lastReportedTombstoneTimestamp != null
195+
&& latestTombstone.getTimestamp() <= lastReportedTombstoneTimestamp) {
196+
options
197+
.getLogger()
198+
.log(SentryLevel.DEBUG, "Latest Tombstone has already been reported, returning early.");
175199
return;
176200
}
177201

178-
reportAsSentryEvent(latestTombstone);
202+
if (options.isReportHistoricalTombstones()) {
203+
// report the remainder without enriching
204+
reportNonEnrichedHistoricalTombstones(exitInfos, lastReportedTombstoneTimestamp);
205+
}
206+
207+
// report the latest Tombstone with enriching, if contexts are available, otherwise report it
208+
// non-enriched
209+
reportAsSentryEvent(latestTombstone, true);
179210
}
180211

181212
@RequiresApi(api = Build.VERSION_CODES.R)
182-
private void reportAsSentryEvent(ApplicationExitInfo exitInfo) {
213+
private void reportNonEnrichedHistoricalTombstones(
214+
List<ApplicationExitInfo> exitInfos, @Nullable Long lastReportedTombstoneTimestamp) {
215+
// we reverse the list, because the OS puts errors in order of appearance, last-to-first
216+
// and we want to write a marker file after each ANR has been processed, so in case the app
217+
// gets killed meanwhile, we can proceed from the last reported ANR and not process the entire
218+
// list again
219+
Collections.reverse(exitInfos);
220+
for (ApplicationExitInfo applicationExitInfo : exitInfos) {
221+
if (applicationExitInfo.getReason() == ApplicationExitInfo.REASON_CRASH_NATIVE) {
222+
if (applicationExitInfo.getTimestamp() < threshold) {
223+
options
224+
.getLogger()
225+
.log(SentryLevel.DEBUG, "Tombstone happened too long ago %s.", applicationExitInfo);
226+
continue;
227+
}
228+
229+
if (lastReportedTombstoneTimestamp != null
230+
&& applicationExitInfo.getTimestamp() <= lastReportedTombstoneTimestamp) {
231+
options
232+
.getLogger()
233+
.log(
234+
SentryLevel.DEBUG,
235+
"Tombstone has already been reported %s.",
236+
applicationExitInfo);
237+
continue;
238+
}
239+
240+
reportAsSentryEvent(applicationExitInfo, false); // do not enrich past events
241+
}
242+
}
243+
}
244+
245+
@RequiresApi(api = Build.VERSION_CODES.R)
246+
private void reportAsSentryEvent(ApplicationExitInfo exitInfo, boolean enrich) {
183247
SentryEvent event;
184248
try {
185-
TombstoneParser parser = new TombstoneParser(exitInfo.getTraceInputStream());
249+
InputStream tombstoneInputStream = exitInfo.getTraceInputStream();
250+
if (tombstoneInputStream == null) {
251+
logTombstoneFailure(exitInfo);
252+
return;
253+
}
254+
255+
final TombstoneParser parser = new TombstoneParser(tombstoneInputStream);
186256
event = parser.parse();
187-
event.setTimestamp(DateUtils.getDateTime(exitInfo.getTimestamp()));
188257
} catch (IOException e) {
189-
throw new RuntimeException(e);
258+
logTombstoneFailure(exitInfo);
259+
return;
260+
}
261+
262+
if (event == null) {
263+
logTombstoneFailure(exitInfo);
264+
return;
190265
}
191266

192-
scopes.captureEvent(event);
267+
final long tombstoneTimestamp = exitInfo.getTimestamp();
268+
event.setTimestamp(DateUtils.getDateTime(tombstoneTimestamp));
269+
270+
final TombstoneHint tombstoneHint =
271+
new TombstoneHint(
272+
options.getFlushTimeoutMillis(), options.getLogger(), tombstoneTimestamp, enrich);
273+
final Hint hint = HintUtils.createWithTypeCheckHint(tombstoneHint);
274+
275+
final @NotNull SentryId sentryId = scopes.captureEvent(event, hint);
276+
final boolean isEventDropped = sentryId.equals(SentryId.EMPTY_ID);
277+
if (!isEventDropped) {
278+
// Block until the event is flushed to disk and the last_reported_tombstone marker is
279+
// updated
280+
if (!tombstoneHint.waitFlush()) {
281+
options
282+
.getLogger()
283+
.log(
284+
SentryLevel.WARNING,
285+
"Timed out waiting to flush Tombstone event to disk. Event: %s",
286+
event.getEventId());
287+
}
288+
}
289+
}
290+
291+
@RequiresApi(api = Build.VERSION_CODES.R)
292+
private void logTombstoneFailure(ApplicationExitInfo exitInfo) {
293+
options
294+
.getLogger()
295+
.log(
296+
SentryLevel.WARNING,
297+
"Native crash report from %s does not contain a valid tombstone.",
298+
DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(exitInfo.getTimestamp())));
299+
}
300+
}
301+
302+
@ApiStatus.Internal
303+
public static final class TombstoneHint extends BlockingFlushHint
304+
implements Backfillable, NativeCrashExit {
305+
306+
private final long tombstoneTimestamp;
307+
private final boolean shouldEnrich;
308+
309+
public TombstoneHint(
310+
long flushTimeoutMillis,
311+
@NotNull ILogger logger,
312+
long tombstoneTimestamp,
313+
boolean shouldEnrich) {
314+
super(flushTimeoutMillis, logger);
315+
this.tombstoneTimestamp = tombstoneTimestamp;
316+
this.shouldEnrich = shouldEnrich;
193317
}
318+
319+
@Override
320+
public Long timestamp() {
321+
return tombstoneTimestamp;
322+
}
323+
324+
@Override
325+
public boolean shouldEnrich() {
326+
return shouldEnrich;
327+
}
328+
329+
@Override
330+
public boolean isFlushable(@Nullable SentryId eventId) {
331+
return true;
332+
}
333+
334+
@Override
335+
public void setFlushable(@NotNull SentryId eventId) {}
194336
}
195337
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.sentry.hints;
2+
3+
import org.jetbrains.annotations.ApiStatus;
4+
import org.jetbrains.annotations.Nullable;
5+
6+
/**
7+
* This is not sensationally useful right now. It only exists as marker interface to distinguish Tombstone events from AbnormalExits, which
8+
* they are not. The timestamp is used to record the timestamp of the last reported native crash we retrieved from the ApplicationExitInfo.
9+
*/
10+
@ApiStatus.Internal
11+
public interface NativeCrashExit {
12+
/** When exactly the crash exit happened */
13+
@Nullable
14+
Long timestamp();
15+
}

0 commit comments

Comments
 (0)