From cd9e945e556ca14638180f3258be68f9c2cc9651 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 4 Aug 2026 15:32:03 -0300 Subject: [PATCH 1/2] 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) From 3b6a1d2ef6c205eb9950845a9fac1a27a3a95f7b Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 11 Aug 2026 21:18:32 -0300 Subject: [PATCH 2/2] fix(android): Address PR review comments on tombstone breadcrumb dedup Prefer persisted breadcrumbs when the event's own list is empty (not just null), skip the disk read when the event already has breadcrumbs, and apply the same isEmpty() check in SentryClient.applyScope for Cached events. Adds the changelog entry for #5888. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01RAtobZMVnCPgeP2fmvrr8a --- CHANGELOG.md | 1 + .../core/ApplicationExitInfoEventProcessor.java | 13 ++++++++----- sentry/src/main/java/io/sentry/SentryClient.java | 3 ++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ced1fee97..5e0852091b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixes +- Prevent duplicated breadcrumbs on tombstone-merged native crash events ([#5888](https://github.com/getsentry/sentry-java/pull/5888)) - Prevent inflated cold app start when the OS spawns the process in the background (e.g. FCM push) on API 35+ ([#5841](https://github.com/getsentry/sentry-java/pull/5841), [#5880](https://github.com/getsentry/sentry-java/pull/5880)) - Preserve single-sample ANR profile chunks so profiles remain available on ANR events ([#5872](https://github.com/getsentry/sentry-java/pull/5872)) - Avoid a CPU busy-loop when recording discarded log or metric envelopes under rate limiting ([#5835](https://github.com/getsentry/sentry-java/pull/5835)) 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 6dcdf4d8de..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: the event already carries its own breadcrumbs (e.g. a tombstone-merged native - // crash event), so appending the persisted ones here would duplicate entries. + event.setBreadcrumbs(breadcrumbs); } @SuppressWarnings("unchecked") 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