88import android .os .Build ;
99import androidx .annotation .RequiresApi ;
1010import io .sentry .DateUtils ;
11+ import io .sentry .Hint ;
12+ import io .sentry .ILogger ;
1113import io .sentry .IScopes ;
1214import io .sentry .Integration ;
1315import io .sentry .SentryEvent ;
1416import io .sentry .SentryLevel ;
1517import io .sentry .SentryOptions ;
18+ import io .sentry .android .core .cache .AndroidEnvelopeCache ;
1619import io .sentry .android .core .internal .tombstone .TombstoneParser ;
1720import io .sentry .cache .EnvelopeCache ;
1821import 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 ;
1926import io .sentry .transport .CurrentDateProvider ;
2027import io .sentry .transport .ICurrentDateProvider ;
28+ import io .sentry .util .HintUtils ;
2129import io .sentry .util .Objects ;
2230import java .io .Closeable ;
2331import java .io .IOException ;
32+ import java .io .InputStream ;
33+ import java .time .Instant ;
34+ import java .time .format .DateTimeFormatter ;
2435import java .util .ArrayList ;
36+ import java .util .Collections ;
2537import java .util .List ;
2638import java .util .concurrent .TimeUnit ;
2739import 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}
0 commit comments