From cd9e945e556ca14638180f3258be68f9c2cc9651 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 4 Aug 2026 15:32:03 -0300 Subject: [PATCH] fix(android): Prevent duplicated breadcrumbs on tombstone-merged events Tombstone-merged native crash events already carry their own breadcrumb history from crash time; backfilling persisted scope breadcrumbs on top of them duplicated entries. Skip the backfill when the event already has breadcrumbs, matching the guard already used for cached native events in SentryClient.applyScope(). Co-Authored-By: Claude --- .../ApplicationExitInfoEventProcessor.java | 4 ++-- .../ApplicationExitInfoEventProcessorTest.kt | 22 ++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) 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..6dcdf4d8de 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 @@ -356,9 +356,9 @@ private void setBreadcrumbs(final @NotNull SentryBaseEvent event) { } if (event.getBreadcrumbs() == null) { event.setBreadcrumbs(breadcrumbs); - } else { - event.getBreadcrumbs().addAll(breadcrumbs); } + // else: the event already carries its own breadcrumbs (e.g. a tombstone-merged native + // crash event), so appending the persisted ones here would duplicate entries. } @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)