feat(android): [Unhandled Sessions 3] Add internal non-terminating envelope capture - #5921
feat(android): [Unhandled Sessions 3] Add internal non-terminating envelope capture#5921buenaflor wants to merge 32 commits into
Conversation
🚨 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:
|
|
🚨 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:
|
📲 Install BuildsAndroid
|
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| a416a65 | 333.78 ms | 410.37 ms | 76.59 ms |
| 2195398 | 319.02 ms | 342.38 ms | 23.36 ms |
| 62b579c | 349.26 ms | 426.26 ms | 77.00 ms |
| bbc35bb | 324.88 ms | 425.73 ms | 100.85 ms |
| e59e22a | 374.68 ms | 442.14 ms | 67.46 ms |
| 806307f | 357.85 ms | 424.64 ms | 66.79 ms |
| 62b579c | 312.88 ms | 361.57 ms | 48.70 ms |
| 8687935 | 332.52 ms | 362.23 ms | 29.71 ms |
| 1edbdfa | 364.77 ms | 450.29 ms | 85.52 ms |
| 2195398 | 322.52 ms | 361.91 ms | 39.39 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| a416a65 | 1.58 MiB | 2.12 MiB | 555.26 KiB |
| 2195398 | 0 B | 0 B | 0 B |
| 62b579c | 0 B | 0 B | 0 B |
| bbc35bb | 1.58 MiB | 2.12 MiB | 553.01 KiB |
| e59e22a | 1.58 MiB | 2.20 MiB | 635.34 KiB |
| 806307f | 1.58 MiB | 2.10 MiB | 533.42 KiB |
| 62b579c | 0 B | 0 B | 0 B |
| 8687935 | 1.58 MiB | 2.19 MiB | 619.17 KiB |
| 1edbdfa | 1.58 MiB | 2.20 MiB | 635.34 KiB |
| 2195398 | 0 B | 0 B | 0 B |
🚨 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:
|
6 similar comments
🚨 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:
|
🚨 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:
|
🚨 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:
|
🚨 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:
|
🚨 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:
|
🚨 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:
|
56ab999 to
1906ec6
Compare
🚨 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:
|
2 similar comments
🚨 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:
|
🚨 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:
|
06f5823 to
ae23921
Compare
🚨 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:
|
ae23921 to
b2ff9d9
Compare
🚨 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:
|
b2ff9d9 to
945602b
Compare
🚨 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:
|
…ions-internal-api
🚨 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:
|
…ions-internal-api
🚨 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:
|
captureEnvelopeNonTerminating called persistCurrentSession from inside the withSession callback, so synchronous session-file I/O ran while holding the scope sessionLock, on the platform thread for Flutter. Mutate under the lock and write the clone withSession returns, the way SentryClient.updateSessionData uses its snapshot. 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:
|
…lock" This reverts commit da288f8.
🚨 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:
|
…ions-internal-api
🚨 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:
|
Three reviewers in a row read the persist as accidentally holding the scope lock, so say why it is deliberate. Also name abnormal alongside crashed as a terminal status that wins over the unhandled marker, following the Session javadoc. 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:
|
runningcode
left a comment
There was a problem hiding this comment.
This looks good aside from the exception handling!
| final SentryEnvelope repackagedEnvelope = | ||
| new SentryEnvelope(envelope.getHeader(), envelopeItems); | ||
| return scopes.captureEnvelope(repackagedEnvelope); | ||
| } catch (Throwable t) { |
There was a problem hiding this comment.
since we arent doing disk I/O inside the try/catch, why do we need this? which parts can throw Exceptions ?
There was a problem hiding this comment.
there is disk I/O (deleteCurrentSessionFile)
however due to eventStateOf which declares throws Exception we can't narrow it down
should we use rethrowIfFatal? it's referenced in the AGENTS.md
There was a problem hiding this comment.
IMO, we should only capture IOException if that is the only exception that can happen here. otherwise we will silenty drop errors that we don't expect and we won't know if this feature works.
The envelope reader declares IOException and rejects malformed payloads with an unchecked IllegalArgumentException, so name both instead of catching Exception. 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:
|
Pull the session mutation out of captureEnvelopeNonTerminating so the dropped-event API can reuse it without duplicating the persist path. 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:
|
…nto feat/unhandled-sessions-internal-api
🚨 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:
|
runningcode
left a comment
There was a problem hiding this comment.
Hey, looks like there are some concurrency issues with this change. Let me know if you have questions and we can go through them together!
| final SentryEnvelope repackagedEnvelope = | ||
| new SentryEnvelope(envelope.getHeader(), envelopeItems); | ||
| return scopes.captureEnvelope(repackagedEnvelope); | ||
| } catch (Throwable t) { |
There was a problem hiding this comment.
IMO, we should only capture IOException if that is the only exception that can happen here. otherwise we will silenty drop errors that we don't expect and we won't know if this feature works.
Every writer to the live session now holds Scope.sessionLock, and only clones leave it, so the lazy serialization on the transport thread and the session file write can no longer race a later mutation. Persisting moves to the executor service instead of running on the calling thread, an already terminated session is no longer written back to the session file, and the broad catch is narrowed to the one call that forces it. Co-authored-by: Cursor <cursoragent@cursor.com>
end() already gives a terminal status precedence via its status == Ok guard, so the clearing is about keeping the flag honest: without it a crashed session serializes a marker claiming it did not terminate, and the public accessor reports true for it. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f356f78. Configure here.
| options.getExecutorService().submit(() -> cache.persistCurrentSession(session)); | ||
| } catch (RejectedExecutionException e) { | ||
| options.getLogger().log(WARNING, "Submission of session persisting rejected.", e); | ||
| } |
There was a problem hiding this comment.
Async persist overwrites rotated sessions
High Severity
schedulePersistSession writes a cloned snapshot on SentryExecutorService after withSession releases, while session start/end still mutate session.json on the transport executor. A delayed persist can rewrite an already-ended session (duplicate unhandled on next launch) or overwrite a newer current session. The flag can also miss disk entirely if the process dies before the queued write runs, which is the guarantee this path exists for.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit f356f78. Configure here.

PR Stack (Unhandled Sessions)
📜 Description
Adds
InternalSentrySdk.captureEnvelopeNonTerminating(byte[])for hybrid runtimes where an unhandled exception does not terminate the process.Unlike
captureEnvelope(byte[], boolean), it does not treathandled=falseas a crash. Instead it:Okwith the same session id on the scope,The session is finalized later by normal lifecycle (
endSession, background, or previous-session recovery) asunhandled, unless a terminal status takes over first, such ascrashedfor a native crash orabnormalfor an ANR.captureEnvelope(byte[], boolean)is unchanged.Also in this PR:
Scope.IWithSessionbecomes public soInternalSentrySdkcan mutate the session under the scope lock; it is the only consumer. It carries@ApiStatus.InternallikeIWithTransactionandIWithPropagationContext, so it is not supported API despite being public.eventStateOfreturningEnvelopeEventState, replacing two loops that computed the same booleans by different routes, plus a sharedreadEnvelope.Exceptionrather thanThrowable, soOutOfMemoryErrorand friends propagate instead of being swallowed. The existingcaptureEnvelopekeeps itscatch (Throwable).💡 Motivation and Context
Flutter forwards
handled=falseevents through the terminating hybrid capture path. That marks the sessioncrashedand may start a replacement session even though the Flutter process keeps running, incorrectly lowering crash-free session rates.💚 How did you test it?
New
InternalSentrySdkTestcoverage: the session stayingOkwith the same id and the flag persisted to disk;endSessionafterwards finalizing asunhandled; and a later hard crash finalizing the old session ascrashedand starting a freshOksession. ExistingcaptureEnvelopetests confirm that path is unchanged.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
Update the Flutter Android bridge to use
captureEnvelopeNonTerminatingfor non-terminating unhandled events.