From c9ea8d158e2e4ce82256bb59c576710b12af3da2 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 58dbc7ab8d..1ebc9fc3b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,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 cb612cd7b9..0178650459 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 @@ -264,6 +264,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 d41c2c8ad60dcfc1efeea3ff4b84c4e5d60a46e0 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 1ebc9fc3b6..5991465911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,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 980ad51d633ca8d75ec95591e636f3d2b67e6ce0 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 5991465911..823c43f992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,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 0178650459..2ded6880ce 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 @@ -266,7 +266,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 a40db5d10231061ec979284505b6ee64dad99d37 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 2ded6880ce..a91225f32c 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,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}) */