From 27b3c881de69d09948e20836e1613b8e75c3cb29 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 25 Aug 2026 12:13:12 +0200 Subject: [PATCH 1/4] feat(android): Add session update for dropped hybrid errors Hybrid SDKs skip captureEnvelopeNonTerminating when an error is unsampled, so the session never records it. Expose the same non-terminating session update without sending the event. Co-authored-by: Cursor --- CHANGELOG.md | 1 + .../api/sentry-android-core.api | 1 + .../android/core/InternalSentrySdk.java | 13 +++ .../android/core/InternalSentrySdkTest.kt | 80 +++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd4e4e534..9d2031a20c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ### Internal - Add `InternalSentrySdk.captureEnvelopeNonTerminating` for hybrid SDKs (e.g. Flutter) so unhandled exceptions that don't terminate the process no longer end the session as `crashed` ([#5921](https://github.com/getsentry/sentry-java/pull/5921)) +- Add `InternalSentrySdk.updateSessionForDroppedEventNonTerminating` so hybrid SDKs can still update the session when an error is dropped by sampling or rate limiting ## 8.54.0 diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index f2162e72c5..40342d98a0 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -323,6 +323,7 @@ public final class io/sentry/android/core/InternalSentrySdk { public static fun getCurrentScope ()Lio/sentry/IScope; public static fun serializeScope (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/IScope;)Ljava/util/Map; public static fun setTrace (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)V + public static fun updateSessionForDroppedEventNonTerminating (Z)V } public final class io/sentry/android/core/LoadClass : io/sentry/util/LoadClass { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index 63891d1174..cad15bf69e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -270,6 +270,19 @@ public static SentryId captureEnvelopeNonTerminating(final @NotNull byte[] envel return scopes.captureEnvelope(envelope); } + /** + * Session side effects of {@link #captureEnvelopeNonTerminating(byte[])} without sending the + * event. Hybrid SDKs should call this when an error is dropped by sample rate or rate limiting. + * + *

Do not call this for events dropped by {@code beforeSend} or ignored exception types. + * + * @param crashed {@code true} if the dropped error was unhandled ({@code + * mechanism.handled=false}) + */ + public static void updateSessionForDroppedEventNonTerminating(final boolean crashed) { + updateSessionNonTerminating(crashed); + } + /** * Flags the current session for a non-terminating hybrid error and persists it before returning, * so the marker survives an immediate process death. diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt index 88b54ce7c8..870b71855f 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt @@ -612,6 +612,86 @@ class InternalSentrySdkTest { assertThat(activeSession.get().sessionId).isNotEqualTo(oldSid) } + @Test + fun `updateSessionForDroppedEventNonTerminating flags an unhandled error without sending an envelope`() { + val fixture = Fixture() + fixture.init(context) + + val originalSid = AtomicReference() + Sentry.configureScope { scope -> originalSid.set(scope.session!!.sessionId) } + fixture.capturedEnvelopes.clear() + + InternalSentrySdk.updateSessionForDroppedEventNonTerminating(true) + + assertThat(fixture.capturedEnvelopes).isEmpty() + + val scopeSession = AtomicReference() + Sentry.configureScope { scope -> scopeSession.set(scope.session) } + assertThat(scopeSession.get().status).isEqualTo(Session.State.Ok) + assertThat(scopeSession.get().hasNonTerminatingUnhandledError()).isTrue() + assertThat(scopeSession.get().errorCount()).isEqualTo(1) + assertThat(scopeSession.get().sessionId).isEqualTo(originalSid.get()) + + val sessionFile = EnvelopeCache.getCurrentSessionFile(fixture.options.cacheDirPath!!) + val persistedSession = + fixture.options.serializer.deserialize(sessionFile.reader(), Session::class.java)!! + assertThat(persistedSession.status).isEqualTo(Session.State.Ok) + assertThat(persistedSession.hasNonTerminatingUnhandledError()).isTrue() + assertThat(persistedSession.errorCount()).isEqualTo(1) + assertThat(persistedSession.sessionId).isEqualTo(originalSid.get()) + } + + @Test + fun `updateSessionForDroppedEventNonTerminating increments errors for a handled error without sending an envelope`() { + val fixture = Fixture() + fixture.init(context) + + val originalSid = AtomicReference() + Sentry.configureScope { scope -> originalSid.set(scope.session!!.sessionId) } + fixture.capturedEnvelopes.clear() + + InternalSentrySdk.updateSessionForDroppedEventNonTerminating(false) + + assertThat(fixture.capturedEnvelopes).isEmpty() + + val scopeSession = AtomicReference() + Sentry.configureScope { scope -> scopeSession.set(scope.session) } + assertThat(scopeSession.get().status).isEqualTo(Session.State.Ok) + assertThat(scopeSession.get().hasNonTerminatingUnhandledError()).isFalse() + assertThat(scopeSession.get().errorCount()).isEqualTo(1) + assertThat(scopeSession.get().sessionId).isEqualTo(originalSid.get()) + + val sessionFile = EnvelopeCache.getCurrentSessionFile(fixture.options.cacheDirPath!!) + val persistedSession = + fixture.options.serializer.deserialize(sessionFile.reader(), Session::class.java)!! + assertThat(persistedSession.status).isEqualTo(Session.State.Ok) + assertThat(persistedSession.hasNonTerminatingUnhandledError()).isFalse() + assertThat(persistedSession.errorCount()).isEqualTo(1) + } + + @Test + fun `updateSessionForDroppedEventNonTerminating then endSession finalizes the session as unhandled`() { + val fixture = Fixture() + fixture.init(context) + + InternalSentrySdk.updateSessionForDroppedEventNonTerminating(true) + fixture.capturedEnvelopes.clear() + + Sentry.endSession() + + val sessionItems = + fixture.capturedEnvelopes + .flatMap { it.items.toList() } + .filter { it.header.type == SentryItemType.Session } + assertThat(sessionItems).hasSize(1) + val endedSession = + fixture.options.serializer.deserialize( + InputStreamReader(ByteArrayInputStream(sessionItems[0].data)), + Session::class.java, + )!! + assertThat(endedSession.status).isEqualTo(Session.State.Unhandled) + } + @Test fun `getAppStartMeasurement returns correct serialized data from the app start instance`() { Fixture().mockFinishedAppStart() From bab3e46ce4d84a54e0be728c317a6ddd035340eb Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 25 Aug 2026 12:44:15 +0200 Subject: [PATCH 2/4] changelog Co-authored-by: Cursor --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d2031a20c..89ee4883e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ ### Internal - Add `InternalSentrySdk.captureEnvelopeNonTerminating` for hybrid SDKs (e.g. Flutter) so unhandled exceptions that don't terminate the process no longer end the session as `crashed` ([#5921](https://github.com/getsentry/sentry-java/pull/5921)) -- Add `InternalSentrySdk.updateSessionForDroppedEventNonTerminating` so hybrid SDKs can still update the session when an error is dropped by sampling or rate limiting +- Add `InternalSentrySdk.updateSessionForDroppedEventNonTerminating` so hybrid SDKs can still update the session when an error is dropped by sampling or rate limiting ([#5990](https://github.com/getsentry/sentry-java/pull/5990)) ## 8.54.0 From b55a21f5bce73c911b557cbfdd7d994a50bbe7ca Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 25 Aug 2026 14:29:56 +0200 Subject: [PATCH 3/4] docs(android): Mention only sampling for the dropped-event session API This path is for hybrid errors dropped by sample rate, not rate limiting. Co-authored-by: Cursor --- CHANGELOG.md | 2 +- .../src/main/java/io/sentry/android/core/InternalSentrySdk.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89ee4883e7..f54e9aaf7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ ### Internal - Add `InternalSentrySdk.captureEnvelopeNonTerminating` for hybrid SDKs (e.g. Flutter) so unhandled exceptions that don't terminate the process no longer end the session as `crashed` ([#5921](https://github.com/getsentry/sentry-java/pull/5921)) -- Add `InternalSentrySdk.updateSessionForDroppedEventNonTerminating` so hybrid SDKs can still update the session when an error is dropped by sampling or rate limiting ([#5990](https://github.com/getsentry/sentry-java/pull/5990)) +- Add `InternalSentrySdk.updateSessionForDroppedEventNonTerminating` so hybrid SDKs can still update the session when an error is dropped by sampling ([#5990](https://github.com/getsentry/sentry-java/pull/5990)) ## 8.54.0 diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index cad15bf69e..5506d96ea9 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -272,7 +272,7 @@ public static SentryId captureEnvelopeNonTerminating(final @NotNull byte[] envel /** * Session side effects of {@link #captureEnvelopeNonTerminating(byte[])} without sending the - * event. Hybrid SDKs should call this when an error is dropped by sample rate or rate limiting. + * event. Hybrid SDKs should call this when an error is dropped by sampling. * *

Do not call this for events dropped by {@code beforeSend} or ignored exception types. * From 2d6df91b4a236dc4d95994cc5bae6752101aef99 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 28 Aug 2026 13:49:06 +0200 Subject: [PATCH 4/4] docs(android): Note that the dropped-event session API writes to disk Persisting the session is synchronous so the unhandled marker is durable by the time the call returns, which means callers must stay off the main thread. Say so where hybrid SDK authors will read it. Co-authored-by: Cursor --- .../main/java/io/sentry/android/core/InternalSentrySdk.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index 5506d96ea9..ba3afc4377 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -276,6 +276,10 @@ public static SentryId captureEnvelopeNonTerminating(final @NotNull byte[] envel * *

Do not call this for events dropped by {@code beforeSend} or ignored exception types. * + *

Persisting the session is a blocking disk write on the calling thread, so call this off the + * main thread as the hybrid SDKs do. It is synchronous on purpose: a deferred write would not be + * on disk yet if the process dies right after this returns. + * * @param crashed {@code true} if the dropped error was unhandled ({@code * mechanism.handled=false}) */