Skip to content

Commit db2e82a

Browse files
authored
fix(android): mark tombstone and ANR exits as reported when the event is dropped (#6002)
1 parent ecf86ff commit db2e82a

14 files changed

Lines changed: 152 additions & 9 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Keep dropped tombstone and ANR events dropped, instead of reporting the same app exit again at every app start ([#6002](https://github.com/getsentry/sentry-java/pull/6002))
8+
39
## 8.54.0
410

511
### Features

‎sentry-android-core/api/sentry-android-core.api‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,7 @@ public class io/sentry/android/core/TombstoneIntegration$TombstonePolicy : io/se
638638
public fun getLabel ()Ljava/lang/String;
639639
public fun getLastReportedTimestamp ()Ljava/lang/Long;
640640
public fun getTargetReason ()I
641+
public fun markReported (J)V
641642
public fun shouldReportHistorical ()Z
642643
}
643644

@@ -749,6 +750,8 @@ public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry
749750
public static fun hasStartupCrashMarker (Lio/sentry/SentryOptions;)Z
750751
public static fun lastReportedAnr (Lio/sentry/SentryOptions;)Ljava/lang/Long;
751752
public static fun lastReportedTombstone (Lio/sentry/SentryOptions;)Ljava/lang/Long;
753+
public static fun markAnrReported (Lio/sentry/SentryOptions;J)V
754+
public static fun markTombstoneReported (Lio/sentry/SentryOptions;J)V
752755
public fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V
753756
public fun storeEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Z
754757
}

‎sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ public boolean shouldReportHistorical() {
132132
return AndroidEnvelopeCache.lastReportedAnr(options);
133133
}
134134

135+
@Override
136+
public void markReported(final long timestamp) {
137+
AndroidEnvelopeCache.markAnrReported(options, timestamp);
138+
}
139+
135140
@Override
136141
public @Nullable ApplicationExitInfoHistoryDispatcher.Report buildReport(
137142
final @NotNull ApplicationExitInfo exitInfo, final boolean shouldEnrich) {

‎sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoHistoryDispatcher.java‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.sentry.hints.BlockingFlushHint;
1515
import io.sentry.protocol.SentryId;
1616
import io.sentry.transport.ICurrentDateProvider;
17+
import io.sentry.util.HintUtils;
1718
import java.util.ArrayList;
1819
import java.util.Collections;
1920
import java.util.Iterator;
@@ -177,6 +178,7 @@ private void reportHistorical(
177178
}
178179
}
179180

181+
@RequiresApi(api = Build.VERSION_CODES.R)
180182
private void report(final @NotNull ApplicationExitInfo exitInfo, final boolean enrich) {
181183
final @Nullable Report report = policy.buildReport(exitInfo, enrich);
182184

@@ -186,7 +188,28 @@ private void report(final @NotNull ApplicationExitInfo exitInfo, final boolean e
186188

187189
final @NotNull SentryId sentryId = scopes.captureEvent(report.getEvent(), report.getHint());
188190
final boolean isEventDropped = sentryId.equals(SentryId.EMPTY_ID);
189-
if (!isEventDropped) {
191+
if (isEventDropped) {
192+
// A dropped event never reaches the envelope disk cache, which is where the last reported
193+
// marker is normally written. Without writing it here, the very same exit would be turned
194+
// into an event again on the next app start, no matter why it was dropped. Only a technical
195+
// failure to hand the event over keeps the exit eligible for another attempt.
196+
if (HintUtils.isCaptureFailed(report.getHint())) {
197+
options
198+
.getLogger()
199+
.log(
200+
SentryLevel.DEBUG,
201+
"Capturing the %s event failed, leaving the exit for the next app start.",
202+
policy.getLabel());
203+
} else {
204+
options
205+
.getLogger()
206+
.log(
207+
SentryLevel.DEBUG,
208+
"%s event was dropped, marking the exit as reported.",
209+
policy.getLabel());
210+
policy.markReported(exitInfo.getTimestamp());
211+
}
212+
} else {
190213
final @Nullable BlockingFlushHint flushHint = report.getFlushHint();
191214
if (flushHint != null && !flushHint.waitFlush()) {
192215
options
@@ -211,6 +234,9 @@ interface ApplicationExitInfoPolicy {
211234
@Nullable
212235
Long getLastReportedTimestamp();
213236

237+
/** Records {@code timestamp} as the last reported exit, so it is not reported again. */
238+
void markReported(long timestamp);
239+
214240
@Nullable
215241
Report buildReport(@NotNull ApplicationExitInfo exitInfo, boolean enrich);
216242
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ public boolean shouldReportHistorical() {
147147
return AndroidEnvelopeCache.lastReportedTombstone(options);
148148
}
149149

150+
@Override
151+
public void markReported(final long timestamp) {
152+
AndroidEnvelopeCache.markTombstoneReported(options, timestamp);
153+
}
154+
150155
@RequiresApi(api = Build.VERSION_CODES.R)
151156
@Override
152157
public @Nullable ApplicationExitInfoHistoryDispatcher.Report buildReport(

‎sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ private boolean storeInternalAndroid(@NotNull SentryEnvelope envelope, @NotNull
8585
}
8686

8787
for (TimestampMarkerHandler<?> handler : TIMESTAMP_MARKER_HANDLERS) {
88-
handler.handle(this, hint, options);
88+
handler.handle(hint, options);
8989
}
9090

9191
return didStore;
@@ -182,7 +182,8 @@ public static boolean hasStartupCrashMarker(final @NotNull SentryOptions options
182182
return null;
183183
}
184184

185-
private void writeLastReportedMarker(
185+
private static void writeLastReportedMarker(
186+
final @NotNull SentryOptions options,
186187
final @Nullable Long timestamp,
187188
@NotNull String reportFilename,
188189
@NotNull String markerCategory) {
@@ -215,6 +216,15 @@ private void writeLastReportedMarker(
215216
return lastReportedMarker(options, LAST_TOMBSTONE_REPORT, LAST_TOMBSTONE_MARKER_LABEL);
216217
}
217218

219+
public static void markAnrReported(final @NotNull SentryOptions options, final long timestamp) {
220+
writeLastReportedMarker(options, timestamp, LAST_ANR_REPORT, LAST_ANR_MARKER_LABEL);
221+
}
222+
223+
public static void markTombstoneReported(
224+
final @NotNull SentryOptions options, final long timestamp) {
225+
writeLastReportedMarker(options, timestamp, LAST_TOMBSTONE_REPORT, LAST_TOMBSTONE_MARKER_LABEL);
226+
}
227+
218228
private static final class TimestampMarkerHandler<T> {
219229
interface TimestampExtractor<T> {
220230
@NotNull
@@ -237,10 +247,7 @@ interface TimestampExtractor<T> {
237247
this.timestampProvider = timestampProvider;
238248
}
239249

240-
void handle(
241-
final @NotNull AndroidEnvelopeCache cache,
242-
final @NotNull Hint hint,
243-
final @NotNull SentryAndroidOptions options) {
250+
void handle(final @NotNull Hint hint, final @NotNull SentryAndroidOptions options) {
244251
HintUtils.runIfHasType(
245252
hint,
246253
type,
@@ -253,7 +260,7 @@ void handle(
253260
"Writing last reported %s marker with timestamp %d",
254261
label,
255262
timestamp);
256-
cache.writeLastReportedMarker(timestamp, reportFilename, label);
263+
writeLastReportedMarker(options, timestamp, reportFilename, label);
257264
});
258265
}
259266
}

‎sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitIntegrationTestBase.kt‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,33 @@ abstract class ApplicationExitIntegrationTestBase<THint : Any> {
187187
.log(any(), argThat { startsWith(config.flushLogPrefix) }, any<Any>())
188188
}
189189

190+
@Test
191+
fun `when latest event was dropped, marks the exit as reported`() {
192+
val integration =
193+
fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp, lastEventId = SentryId.EMPTY_ID)
194+
fixture.addAppExitInfo(timestamp = newTimestamp)
195+
196+
integration.register(fixture.scopes, fixture.options)
197+
198+
assertEquals(newTimestamp.toString(), fixture.lastReportedFile.readText())
199+
}
200+
201+
@Test
202+
fun `when capturing the latest event failed, does not mark the exit as reported`() {
203+
val integration =
204+
fixture.getSut(
205+
tmpDir,
206+
lastReportedTimestamp = oldTimestamp,
207+
lastEventId = SentryId.EMPTY_ID,
208+
captureFailed = true,
209+
)
210+
fixture.addAppExitInfo(timestamp = newTimestamp)
211+
212+
integration.register(fixture.scopes, fixture.options)
213+
214+
assertEquals(oldTimestamp.toString(), fixture.lastReportedFile.readText())
215+
}
216+
190217
@Test
191218
fun `historical exits are reported non-enriched`() {
192219
val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp)
@@ -403,6 +430,7 @@ abstract class ApplicationExitIntegrationTestBase<THint : Any> {
403430
sessionFlushTimeoutMillis: Long = 0L,
404431
lastReportedTimestamp: Long? = null,
405432
lastEventId: SentryId = SentryId(),
433+
captureFailed: Boolean = false,
406434
sessionTrackingEnabled: Boolean = true,
407435
reportHistorical: Boolean = true,
408436
extraOptions: (SentryAndroidOptions) -> Unit = {},
@@ -426,7 +454,12 @@ abstract class ApplicationExitIntegrationTestBase<THint : Any> {
426454
lastReportedFile = File(cacheDir, config.lastReportedFileName)
427455
lastReportedFile.writeText(lastReportedTimestamp.toString())
428456
}
429-
whenever(scopes.captureEvent(any(), anyOrNull<Hint>())).thenReturn(lastEventId)
457+
whenever(scopes.captureEvent(any(), anyOrNull<Hint>())).thenAnswer { invocation ->
458+
if (captureFailed) {
459+
HintUtils.setCaptureFailed(invocation.getArgument(1))
460+
}
461+
lastEventId
462+
}
430463
return config.createIntegration(context)
431464
}
432465

‎sentry/api/sentry.api‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4737,6 +4737,7 @@ public final class io/sentry/TypeCheckHint {
47374737
public static final field OPEN_FEIGN_REQUEST Ljava/lang/String;
47384738
public static final field OPEN_FEIGN_RESPONSE Ljava/lang/String;
47394739
public static final field REPLAY_FRAME_BITMAP Ljava/lang/String;
4740+
public static final field SENTRY_CAPTURE_FAILED Ljava/lang/String;
47404741
public static final field SENTRY_DART_SDK_NAME Ljava/lang/String;
47414742
public static final field SENTRY_DOTNET_SDK_NAME Ljava/lang/String;
47424743
public static final field SENTRY_EVENT_DROP_REASON Ljava/lang/String;
@@ -7762,11 +7763,13 @@ public final class io/sentry/util/HintUtils {
77627763
public static fun getEventDropReason (Lio/sentry/Hint;)Lio/sentry/hints/EventDropReason;
77637764
public static fun getSentrySdkHint (Lio/sentry/Hint;)Ljava/lang/Object;
77647765
public static fun hasType (Lio/sentry/Hint;Ljava/lang/Class;)Z
7766+
public static fun isCaptureFailed (Lio/sentry/Hint;)Z
77657767
public static fun isFromHybridSdk (Lio/sentry/Hint;)Z
77667768
public static fun runIfDoesNotHaveType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryNullableConsumer;)V
77677769
public static fun runIfHasType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryConsumer;)V
77687770
public static fun runIfHasType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryConsumer;Lio/sentry/util/HintUtils$SentryHintFallback;)V
77697771
public static fun runIfHasTypeLogIfNot (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/ILogger;Lio/sentry/util/HintUtils$SentryConsumer;)V
7772+
public static fun setCaptureFailed (Lio/sentry/Hint;)V
77707773
public static fun setEventDropReason (Lio/sentry/Hint;Lio/sentry/hints/EventDropReason;)V
77717774
public static fun setIsFromHybridSdk (Lio/sentry/Hint;Ljava/lang/String;)V
77727775
public static fun setTypeCheckHint (Lio/sentry/Hint;Ljava/lang/Object;)V

‎sentry/src/main/java/io/sentry/Scopes.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,10 @@ public boolean isEnabled() {
152152
.getLogger()
153153
.log(
154154
SentryLevel.WARNING, "Instance is disabled and this 'captureEvent' call is a no-op.");
155+
HintUtils.setCaptureFailed(hint);
155156
} else if (event == null) {
156157
getOptions().getLogger().log(SentryLevel.WARNING, "captureEvent called with null parameter.");
158+
HintUtils.setCaptureFailed(hint);
157159
} else {
158160
try {
159161
assignTraceContext(event);
@@ -166,6 +168,7 @@ public boolean isEnabled() {
166168
.getLogger()
167169
.log(
168170
SentryLevel.ERROR, "Error while capturing event with id: " + event.getEventId(), e);
171+
HintUtils.setCaptureFailed(hint);
169172
}
170173
}
171174
return sentryId;

‎sentry/src/main/java/io/sentry/SentryClient.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul
287287
options.getLogger().log(SentryLevel.WARNING, e, "Capturing event %s failed.", sentryId);
288288

289289
// if there was an error capturing the event, we return an emptyId
290+
HintUtils.setCaptureFailed(hint);
290291
sentryId = SentryId.EMPTY_ID;
291292
}
292293

0 commit comments

Comments
 (0)