diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a45ec3fe3..295904e60d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Prevent duplicated breadcrumbs on tombstone-merged native crash events ([#5888](https://github.com/getsentry/sentry-java/pull/5888)) + ### Dependencies - Bump Native SDK from v0.16.2 to v0.16.3 ([#5962](https://github.com/getsentry/sentry-java/pull/5962)) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java index 3182828a02..31fe2442bf 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java @@ -349,16 +349,19 @@ private void setExtras(final @NotNull SentryBaseEvent event) { @SuppressWarnings("unchecked") private void setBreadcrumbs(final @NotNull SentryBaseEvent event) { + final List eventBreadcrumbs = event.getBreadcrumbs(); + if (eventBreadcrumbs != null && !eventBreadcrumbs.isEmpty()) { + // the event already carries its own breadcrumbs (e.g. a tombstone-merged native + // crash event), so appending the persisted ones here would duplicate entries. Skip the + // disk read altogether since the result would be discarded anyway. + return; + } final List breadcrumbs = (List) readFromDisk(options, BREADCRUMBS_FILENAME, List.class); if (breadcrumbs == null) { return; } - if (event.getBreadcrumbs() == null) { - event.setBreadcrumbs(breadcrumbs); - } else { - event.getBreadcrumbs().addAll(breadcrumbs); - } + event.setBreadcrumbs(breadcrumbs); } @SuppressWarnings("unchecked") diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt index 176ca460eb..f484f994db 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt @@ -334,6 +334,21 @@ class ApplicationExitInfoEventProcessorTest { assertEquals("Google Chrome", processed.contexts.browser!!.name) } + @Test + fun `when backfillable event already has breadcrumbs, does not duplicate them with persisted ones`() { + // simulates a tombstone-merged native crash event, which already carries its own + // breadcrumb history captured at crash time, overlapping with what was persisted to disk + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) + + val processed = + processEvent(hint, populateScopeCache = true) { + breadcrumbs = listOf(Breadcrumb.debug("own-crash-time-breadcrumb")) + } + + assertEquals(1, processed.breadcrumbs!!.size) + assertEquals("own-crash-time-breadcrumb", processed.breadcrumbs!![0].message) + } + @Test fun `when backfillable event is enrichable, does not backfill user ip`() { val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) @@ -670,10 +685,11 @@ class ApplicationExitInfoEventProcessorTest { assertEquals("MainActivity", processed.transaction) assertEquals(DEBUG, processed.level) - assertEquals(3, processed.breadcrumbs!!.size) + // breadcrumbs already set on the event are preserved as-is, not merged with the persisted + // ones, since the event already carries its own authoritative breadcrumb history + assertEquals(1, processed.breadcrumbs!!.size) assertEquals("debug", processed.breadcrumbs!![0].type) - assertEquals("debug", processed.breadcrumbs!![1].type) - assertEquals("navigation", processed.breadcrumbs!![2].type) + assertEquals("test", processed.breadcrumbs!![0].message) assertEquals("debug", processed.environment) assertEquals("io.sentry.samples@1.1.0+220", processed.release) diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 92037f6690..d4922be5dd 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -1618,8 +1618,13 @@ public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metri } } } - if (sentryBaseEvent.getBreadcrumbs() == null) { - sentryBaseEvent.setBreadcrumbs(new ArrayList<>(scope.getBreadcrumbs())); + final List eventBreadcrumbs = sentryBaseEvent.getBreadcrumbs(); + if (eventBreadcrumbs == null || eventBreadcrumbs.isEmpty()) { + // A cached event comes from the outbox; its breadcrumbs (even if empty) belong to a + // past session, so the current scope's breadcrumbs are unrelated and must not be applied. + if (!isCached) { + sentryBaseEvent.setBreadcrumbs(new ArrayList<>(scope.getBreadcrumbs())); + } } else if (!isCached) { // A Cached event comes from the outbox and already carries its own breadcrumbs (e.g. native // events written by sentry-native). Appending the scope's breadcrumbs would duplicate them.