From 4449b9c2ff421ec25ea7f652676a2499e755b0ab Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 17 Jun 2026 15:34:19 +0200 Subject: [PATCH 1/2] fix(android): Prevent breadcrumb loss from numeric timestamp ClassCastException The JS SDK stamps `breadcrumb.timestamp` as a number (epoch seconds), which arrives over the bridge as a `Double`. Since 8.14.0 (#6261) the native `Breadcrumb.Deserializer` reads `timestamp` as an ISO-8601 string and threw a `ClassCastException`, discarding every synced breadcrumb on Android so native crash reports lost breadcrumb context. Normalize a numeric `timestamp` to an ISO-8601 string before deserializing. Fixes #6306 Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 4 ++++ .../RNSentryBreadcrumbTest.kt | 15 +++++++++++++++ .../java/io/sentry/react/RNSentryBreadcrumb.java | 15 ++++++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70405f192d..9e114c31a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ - Record XHR request/response headers and (optionally) bodies in Mobile Session Replay. Opt in via `mobileReplayIntegration` with `networkDetailAllowUrls` to capture headers; set `networkCaptureBodies: true` to also capture bodies. Other options: `networkDetailDenyUrls`, `networkRequestHeaders`, `networkResponseHeaders`. Authorization-like headers are always stripped, bodies are capped at ~150 KB. Covers XHR-based clients like `axios`; fetch will follow. See [Network Details](https://docs.sentry.io/platforms/react-native/session-replay/#network-details) for details. ([#6288](https://github.com/getsentry/sentry-react-native/pull/6288)) - Warn during dev builds when multiple versions of Sentry JS SDK are detected ([#6269](https://github.com/getsentry/sentry-react-native/pull/6269)) +### Fixes + +- Fix Android `ClassCastException` when syncing breadcrumbs with a numeric timestamp to the native scope ([#6306](https://github.com/getsentry/sentry-react-native/issues/6306)) + ### Dependencies - Bump Android SDK from v8.43.1 to v8.43.2 ([#6273](https://github.com/getsentry/sentry-react-native/pull/6273)) diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryBreadcrumbTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryBreadcrumbTest.kt index 46e2f3e069..aa6345f6d9 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryBreadcrumbTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryBreadcrumbTest.kt @@ -68,6 +68,21 @@ class RNSentryBreadcrumbTest { assertNotNull(actual.timestamp) } + @Test + fun parsesNumericTimestampWithoutDiscardingBreadcrumb() { + // The JS SDK stamps `timestamp` as a number (epoch seconds), which arrives over the bridge + // as a Double. The native deserializer expects an ISO-8601 string, so without normalization + // this throws a ClassCastException and the whole breadcrumb is discarded (see #6306). + val map = JavaOnlyMap() + map.putString("message", "testMessage") + map.putDouble("timestamp", 1_700_000_000.5) + val actual = RNSentryBreadcrumb.fromMap(map, logger) + assertEquals("testMessage", actual.message) + assertEquals("react-native", actual.origin) + assertNotNull(actual.timestamp) + assertEquals(1_700_000_000_500L, actual.timestamp.time) + } + @Test fun reactNativeForMissingOrigin() { val map = diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryBreadcrumb.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryBreadcrumb.java index 487f8c3b7e..09ea9b04e2 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryBreadcrumb.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryBreadcrumb.java @@ -4,9 +4,11 @@ import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.ReadableType; import io.sentry.Breadcrumb; +import io.sentry.DateUtils; import io.sentry.ILogger; import io.sentry.SentryLevel; import io.sentry.util.MapObjectReader; +import java.util.Date; import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -42,7 +44,18 @@ public static String getCurrentScreenFrom(ReadableMap from) { @NotNull public static Breadcrumb fromMap(ReadableMap from, @NotNull ILogger logger) { try { - final @NotNull MapObjectReader reader = new MapObjectReader(toDeepHashMap(from)); + final @NotNull Map map = toDeepHashMap(from); + + // The JS SDK stamps `timestamp` as a number (epoch seconds), which arrives over the bridge as + // a Double. The native Breadcrumb deserializer expects an ISO-8601 string, so normalize it + // before deserializing to avoid a ClassCastException. + final @Nullable Object timestamp = map.get("timestamp"); + if (timestamp instanceof Number) { + final long millis = (long) (((Number) timestamp).doubleValue() * 1000); + map.put("timestamp", DateUtils.getTimestamp(new Date(millis))); + } + + final @NotNull MapObjectReader reader = new MapObjectReader(map); final @NotNull Breadcrumb breadcrumb = new Breadcrumb.Deserializer().deserialize(reader, logger); From a8c2748499054991d42180fb06174b997c05e9bc Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 17 Jun 2026 15:38:10 +0200 Subject: [PATCH 2/2] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e114c31a8..01dca9bf65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ ### Fixes -- Fix Android `ClassCastException` when syncing breadcrumbs with a numeric timestamp to the native scope ([#6306](https://github.com/getsentry/sentry-react-native/issues/6306)) +- Fix Android `ClassCastException` when syncing breadcrumbs with a numeric timestamp to the native scope ([#6308](https://github.com/getsentry/sentry-react-native/pull/6308)) ### Dependencies