diff --git a/CHANGELOG.md b/CHANGELOG.md index 29d29a751b..e6d0143828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixes +- Prevent duplicated breadcrumbs on tombstone-merged native crash events ([#5888](https://github.com/getsentry/sentry-java/pull/5888)) - Clear contexts when calling `Scope.clear()` ([#5902](https://github.com/getsentry/sentry-java/pull/5902)) - Preserve custom `Throwable` identities when R8 optimizes Android apps ([#5881](https://github.com/getsentry/sentry-java/pull/5881)) - Report the correct cpu usage for the first performance sample of a transaction, which was measured against the time since device boot ([#5926](https://github.com/getsentry/sentry-java/pull/5926)) 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..1d4f98d817 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -1618,7 +1618,8 @@ public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metri } } } - if (sentryBaseEvent.getBreadcrumbs() == null) { + final List eventBreadcrumbs = sentryBaseEvent.getBreadcrumbs(); + if (eventBreadcrumbs == null || eventBreadcrumbs.isEmpty()) { 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