feat(android): Add internal non-terminating envelope capture #5795
feat(android): Add internal non-terminating envelope capture #5795buenaflor wants to merge 11 commits into
Conversation
…-unhandled sessions Hybrid unhandled exceptions that don't terminate the process (e.g. Flutter) no longer end the session as crashed. The session is kept alive and marked pending-unhandled, finalized as unhandled on session end or escalated to crashed on a native crash. Existing captureEnvelope(byte[], boolean) behavior (React Native) is unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the existing hybrid capture path isolated from non-terminating Flutter session handling while retaining pending-unhandled session persistence. Co-authored-by: Cursor <cursoragent@cursor.com>
Serialize pending session mutations with scope and cache lifecycle operations so delayed session updates cannot overwrite or delete newer session state. Co-authored-by: Cursor <cursoragent@cursor.com>
🚨 Detected changes in high risk code 🚨High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
|
Added unreleased section with recent fixes for ANR and crash events.
|
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit c6f705c. Configure here.
📲 Install BuildsAndroid
|
|
@sentry review |
Check for an unhandled exception directly instead of using the crash-named predicate in a path where the process intentionally continues. Co-authored-by: Cursor <cursoragent@cursor.com>
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| eb95ded | 317.51 ms | 369.08 ms | 51.57 ms |
| 2124a46 | 319.19 ms | 415.04 ms | 95.85 ms |
| c3ee041 | 310.64 ms | 361.90 ms | 51.26 ms |
| dcc6bbf | 382.58 ms | 462.13 ms | 79.54 ms |
| 7a19fee | 315.46 ms | 368.62 ms | 53.16 ms |
| 9054d65 | 330.94 ms | 403.24 ms | 72.30 ms |
| d500866 | 326.13 ms | 378.70 ms | 52.58 ms |
| ad8da22 | 365.86 ms | 427.00 ms | 61.14 ms |
| ed33deb | 337.52 ms | 484.06 ms | 146.54 ms |
| d15471f | 310.26 ms | 377.04 ms | 66.78 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| eb95ded | 0 B | 0 B | 0 B |
| 2124a46 | 1.58 MiB | 2.12 MiB | 551.51 KiB |
| c3ee041 | 0 B | 0 B | 0 B |
| dcc6bbf | 1.58 MiB | 2.12 MiB | 553.10 KiB |
| 7a19fee | 0 B | 0 B | 0 B |
| 9054d65 | 1.58 MiB | 2.29 MiB | 723.38 KiB |
| d500866 | 0 B | 0 B | 0 B |
| ad8da22 | 1.58 MiB | 2.29 MiB | 719.83 KiB |
| ed33deb | 1.58 MiB | 2.13 MiB | 559.52 KiB |
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
Only preserve a mismatched current session when it carries pending unhandled state, retaining existing deletion behavior for other sessions. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@sentry review |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 184e89a. Configure here.
There was a problem hiding this comment.
Pull request overview
This PR adds a non-terminating “unhandled” session path intended for hybrid runtimes (notably Flutter) where handled=false does not imply a process crash. It introduces a locally persisted “pending unhandled” marker on the active session, so the session can later be finalized as unhandled on natural end (unless escalated to crashed/abnormal).
Changes:
- Add
InternalSentrySdk.captureEnvelopeNonTerminating(byte[])(Android) to capture envelopes without ending/replacing the session, while persisting a pending-unhandled marker and incrementing session error count. - Extend
Sessionto support a pending-unhandled lifecycle (mutation, cloning, serialization) and adjust session end/finalization behavior. - Update
EnvelopeCachesession start/end handling to preserve newer same-SID snapshots and avoid deleting a pending session on mismatched session-end envelopes; add broad test coverage across cache/finalizer/session serialization.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| sentry/src/main/java/io/sentry/Session.java | Adds pending-unhandled state tracking, serialization/deserialization, and end()/update() precedence rules. |
| sentry/src/main/java/io/sentry/Scope.java | Exposes Scope.IWithSession as public (still @ApiStatus.Internal) to enable cross-module usage. |
| sentry/src/main/java/io/sentry/cache/EnvelopeCache.java | Improves session start/end disk behavior for same-SID delayed snapshots and pending-unhandled preservation; adds persistCurrentSession. |
| sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java | Adds captureEnvelopeNonTerminating and documents when to use it vs terminating capture. |
| sentry/src/test/java/io/sentry/SessionTest.kt | Adds unit tests for pending-unhandled mutation, end-state precedence, clone behavior, and JSON round-tripping. |
| sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt | Adds protocol-level serialization test for unhandled + pending flag round-trip. |
| sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt | Adds recovery/finalization tests for pending-unhandled vs crash-marker precedence. |
| sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt | Adds extensive session file behavior tests for SID matching/mismatch, null SID edge cases, and terminal precedence. |
| sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt | Adds integration-style tests validating non-terminating behavior, persistence, and later finalization/escalation. |
| sentry/api/sentry.api | Updates API surface snapshots for new/changed public ABI elements. |
| sentry-android-core/api/sentry-android-core.api | Updates API snapshot to include captureEnvelopeNonTerminating. |
| CHANGELOG.md | Adds an “Internal” changelog entry describing the new hybrid non-terminating capture API. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Crashed, | ||
| Abnormal | ||
| Abnormal, | ||
| Unhandled |
There was a problem hiding this comment.
what do ya'll think?
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
🚨 Detected changes in high risk code 🚨High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
|
Only retain a mismatched pending session when its start time proves it is newer than the delayed session end. Co-authored-by: Cursor <cursoragent@cursor.com>
🚨 Detected changes in high risk code 🚨High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
|
| try (final @NotNull ISentryLifecycleToken ignored = sessionLock.acquire()) { | ||
| final @Nullable Session endingSession = readSessionFromEnvelope(envelope); | ||
| final @Nullable Session currentSession = readSessionFromDisk(currentSessionFile); | ||
| final boolean preservePendingSession = | ||
| endingSession != null | ||
| && currentSession != null | ||
| && currentSession.isPendingUnhandled() | ||
| && endingSession.getSessionId() != null | ||
| && currentSession.getSessionId() != null | ||
| && !Objects.equals(endingSession.getSessionId(), currentSession.getSessionId()) | ||
| && endingSession.getStarted() != null | ||
| && currentSession.getStarted() != null | ||
| && currentSession.getStarted().after(endingSession.getStarted()); | ||
| if (!preservePendingSession && !currentSessionFile.delete()) { | ||
| options.getLogger().log(WARNING, "Current envelope doesn't exist."); |
There was a problem hiding this comment.
Instead of deleting session.json unconditionally, we preserve it when a delayed End(A) would delete a newer pending-unhandled session B, confirmed by a different SID and later start time. For example, if End(A) is queued before B’s pending-unhandled state is written, processing End(A) must not delete B’s file. All other cases retain the existing deletion behavior, so this currently only affects state persisted by captureEnvelopeNonTerminating().
| Ok, | ||
| Exited, | ||
| Crashed, | ||
| Abnormal | ||
| Abnormal, | ||
| Unhandled | ||
| } | ||
|
|
||
| /** started timestamp */ |
There was a problem hiding this comment.
Bug: The new State.Unhandled enum is serialized to "unhandled", an invalid session status for the Sentry relay, causing session data to be rejected.
Severity: HIGH
Suggested Fix
Before serializing the session for the envelope, map the internal State.Unhandled to a valid, recognized session status that the Sentry relay can process, such as Abnormal. Alternatively, coordinate with the server team to have "unhandled" added as a valid status to the session protocol if this is intended to be a new standard state.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: sentry/src/main/java/io/sentry/Session.java#L21-L28
Potential issue: The newly introduced `Session.State.Unhandled` enum value is serialized
to the string `"unhandled"` before being sent to the Sentry relay. The relay's ingestion
pipeline only accepts `ok`, `exited`, `crashed`, and `abnormal` as valid session
statuses. As a result, any session finalized with the `Unhandled` status will be
rejected or mishandled by the server. This leads to the loss of session data for
scenarios involving non-terminating unhandled exceptions, undermining the goal of
improving crash-free session rate calculations. The issue occurs when a session with a
pending unhandled exception is finalized.
Also affects:
sentry/src/main/java/io/sentry/Session.java:253~261
📜 Description
Add
InternalSentrySdk.captureEnvelopeNonTerminatingfor hybrid runtimes such as Flutter where an unhandled exception does not terminate the process.The new path keeps the current session alive with the same SID, increments its error count, persists a pending-unhandled marker in the existing session cache, and finalizes it as
unhandledonly when the session naturally ends. ExistingAbnormalandCrashedterminal states remain authoritative, and the existingcaptureEnvelope(byte[], boolean)behavior used by other SDKs is unchanged.💡 Motivation and Context
Flutter currently forwards
handled=falseevents through the terminating hybrid capture path. This marks the session ascrashedand may start a replacement session even though the Flutter process continues running, which lowers crash-free session rates incorrectly.This follows the session protocol's
unhandledstatus for exceptions that are unhandled by the application but do not terminate the process.💚 How did you test it?
📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
captureEnvelopeNonTerminatingfor non-terminating unhandled events.Made with Cursor