From d6d2a210f3e1209c9d8b48d995ac1ae68b44b617 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 11 Feb 2026 19:17:41 +0100 Subject: [PATCH] fix(android): merge tombstone and Native SDK event message. --- CHANGELOG.md | 4 + .../android/core/TombstoneIntegration.java | 8 + .../android/core/TombstoneIntegrationTest.kt | 140 ++++++++++++++++++ 3 files changed, 152 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dd2399e3d..fb1068e7d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Add `installGroupsOverride` parameter to Build Distribution SDK for programmatic filtering, with support for configuration via properties file using `io.sentry.distribution.install-groups-override` ([#5066](https://github.com/getsentry/sentry-java/pull/5066)) +### Fixes + +- When merging tombstones with Native SDK, use the tombstone message if the Native SDK didn't explicitly provide one. ([#5095](https://github.com/getsentry/sentry-java/pull/5095)) + ### Dependencies - Bump Native SDK from v0.12.4 to v0.12.6 ([#5071](https://github.com/getsentry/sentry-java/pull/5071)) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java index b4f2678dc9..f2b8774254 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java @@ -291,6 +291,14 @@ private void mergeNativeCrashes( if (mechanism != null) { mechanism.setType(NativeExceptionMechanism.TOMBSTONE_MERGED.getValue()); } + + // Don't overwrite existing messages in the native event + if (nativeEvent.getMessage() == null + || nativeEvent.getMessage().getMessage() == null + || nativeEvent.getMessage().getMessage().isEmpty()) { + nativeEvent.setMessage(tombstoneEvent.getMessage()); + } + nativeEvent.setExceptions(tombstoneExceptions); nativeEvent.setDebugMeta(tombstoneDebugMeta); nativeEvent.setThreads(tombstoneThreads); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt index 4fe855cd25..3b27d69d08 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt @@ -186,6 +186,146 @@ class TombstoneIntegrationTest : ApplicationExitIntegrationTestBase + val outboxDir = File(options.outboxPath!!) + outboxDir.mkdirs() + createNativeEnvelope(outboxDir, newTimestamp, messageJson = null) + } + + fixture.addAppExitInfo(timestamp = newTimestamp) + integration.register(fixture.scopes, fixture.options) + + verify(fixture.scopes) + .captureEvent( + check { event -> + // Tombstone message should be applied + assertNotNull(event.message) + assertNotNull(event.message!!.formatted) + // The message contains the signal info from the tombstone + assertTrue(event.message!!.formatted!!.contains("Fatal signal")) + }, + any(), + ) + } + + @Test + fun `when native event has message with null template, tombstone message is applied`() { + val integration = + fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp) { options -> + val outboxDir = File(options.outboxPath!!) + outboxDir.mkdirs() + createNativeEnvelope( + outboxDir, + newTimestamp, + messageJson = """{"formatted":"some formatted text"}""", + ) + } + + fixture.addAppExitInfo(timestamp = newTimestamp) + integration.register(fixture.scopes, fixture.options) + + verify(fixture.scopes) + .captureEvent( + check { event -> + // Tombstone message should be applied + assertNotNull(event.message) + assertNotNull(event.message!!.formatted) + assertTrue(event.message!!.formatted!!.contains("Fatal signal")) + }, + any(), + ) + } + + @Test + fun `when native event has message with empty template, tombstone message is applied`() { + val integration = + fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp) { options -> + val outboxDir = File(options.outboxPath!!) + outboxDir.mkdirs() + createNativeEnvelope( + outboxDir, + newTimestamp, + messageJson = """{"message":"","formatted":"some formatted text"}""", + ) + } + + fixture.addAppExitInfo(timestamp = newTimestamp) + integration.register(fixture.scopes, fixture.options) + + verify(fixture.scopes) + .captureEvent( + check { event -> + // Tombstone message should be applied + assertNotNull(event.message) + assertNotNull(event.message!!.formatted) + assertTrue(event.message!!.formatted!!.contains("Fatal signal")) + }, + any(), + ) + } + + @Test + fun `when native event has message with content, native message is preserved`() { + val integration = + fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp) { options -> + val outboxDir = File(options.outboxPath!!) + outboxDir.mkdirs() + createNativeEnvelope( + outboxDir, + newTimestamp, + messageJson = + """{"message":"Native SDK crash message","formatted":"The crash happened at 0xDEADBEEF"}""", + ) + } + + fixture.addAppExitInfo(timestamp = newTimestamp) + integration.register(fixture.scopes, fixture.options) + + verify(fixture.scopes) + .captureEvent( + check { event -> + // Native SDK message should be preserved + assertNotNull(event.message) + assertEquals("Native SDK crash message", event.message!!.message) + assertEquals("The crash happened at 0xDEADBEEF", event.message!!.formatted) + }, + any(), + ) + } + + /** + * Creates a native envelope file with an optional message field. + * + * @param messageJson The JSON for the message field (e.g., + * `{"message":"text","formatted":"text"}`), or null to omit the message field entirely. + */ + private fun createNativeEnvelope( + outboxDir: File, + timestamp: Long, + messageJson: String? = null, + fileName: String = "native-envelope.envelope", + ): File { + val isoTimestamp = DateUtils.getTimestamp(DateUtils.getDateTime(timestamp)) + val messageField = if (messageJson != null) ""","message":$messageJson""" else "" + + val eventJson = + """{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc","timestamp":"$isoTimestamp","platform":"native","level":"fatal"$messageField}""" + val eventJsonSize = eventJson.toByteArray(Charsets.UTF_8).size + + val envelopeContent = + """ + {"event_id":"9ec79c33ec9942ab8353589fcb2e04dc"} + {"type":"event","length":$eventJsonSize,"content_type":"application/json"} + $eventJson + """ + .trimIndent() + + return File(outboxDir, fileName).apply { writeText(envelopeContent) } + } + private fun createNativeEnvelopeWithAttachment(outboxDir: File, timestamp: Long): File { val isoTimestamp = DateUtils.getTimestamp(DateUtils.getDateTime(timestamp))