Skip to content

feat(core): [Unhandled Sessions 1] Add Unhandled session state and non-terminating error flag - #5919

Open
buenaflor wants to merge 12 commits into
feat/unhandled-sessionsfrom
feat/unhandled-sessions-protocol
Open

feat(core): [Unhandled Sessions 1] Add Unhandled session state and non-terminating error flag#5919
buenaflor wants to merge 12 commits into
feat/unhandled-sessionsfrom
feat/unhandled-sessions-protocol

Conversation

@buenaflor

@buenaflor buenaflor commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

PR Stack (Unhandled Sessions)


📜 Description

Adds the session protocol's unhandled status to the Session model.

  • Session.State.Unhandled — an unhandled error the process survived.
  • A hasNonTerminatingUnhandledError flag (non_terminating_unhandled_error in JSON), set by recordNonTerminatingUnhandledError(), which counts the error without ending the session or changing its status while it is alive.
  • end() finalizes a flagged session as Unhandled rather than Exited. Crashed and Abnormal still take precedence, and update(Crashed, ...) clears the flag.
  • clone() and the (de)serializer carry the flag; it is omitted from JSON when unset.

Nothing sets the flag yet, so this PR is inert on its own.

The flag is named for what separates this case from a native crash, which is also unhandled but terminates the process and ends the session as crashed.

💡 Motivation and Context

Hybrid runtimes such as Flutter report handled=false exceptions that do not kill the process. Those currently go through the terminating capture path, which marks the session crashed and starts a replacement even though the app keeps running, incorrectly lowering crash-free session rates.

The protocol has had unhandled for exactly this case since 1.6.0, and Relay accepts it.

💚 How did you test it?

PreviousSessionFinalizerTest covers recovering a flagged previous session, including a native crash escalating it to crashed. SessionSerializationTest covers the JSON round-trip and omission when unset.

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

Persistence across process death in #5920, the capture API in #5921.

#skip-changelog

⚠️ Merge this PR using a merge commit (not squash). Only the collection branch is squash-merged into main.

buenaflor and others added 2 commits August 10, 2026 11:22
Adds Session.State.Unhandled from the session protocol, plus a
pending-unhandled marker that survives serialization. A session carrying
the marker finalizes as Unhandled instead of Exited on end(), while
Crashed and Abnormal keep taking precedence.

Co-authored-by: Cursor <cursoragent@cursor.com>
"Unhandled" alone is ambiguous: a native crash is also an unhandled error, it
just terminates the process and so ends the session as crashed rather than
unhandled. Name the flag after the property that actually distinguishes the two
and match the vocabulary of captureEnvelopeNonTerminating.

Also clarify that the setter only restores the flag when rebuilding a session
and must not be used to mutate a live one.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 2c1d462

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 319.02 ms 369.24 ms 50.22 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
6b019b7 343.31 ms 417.23 ms 73.91 ms
d15471f 286.65 ms 314.68 ms 28.03 ms
d217708 409.83 ms 474.72 ms 64.89 ms
d500866 326.13 ms 378.70 ms 52.58 ms
fcec2f2 314.96 ms 373.66 ms 58.70 ms
d501a7e 314.55 ms 343.34 ms 28.79 ms
7414e9b 322.49 ms 378.88 ms 56.39 ms
fcec2f2 357.47 ms 447.32 ms 89.85 ms
a416a65 316.52 ms 359.67 ms 43.15 ms
983e0f0 350.64 ms 386.44 ms 35.79 ms

App size

Revision Plain With Sentry Diff
6b019b7 0 B 0 B 0 B
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
d217708 1.58 MiB 2.10 MiB 532.97 KiB
d500866 0 B 0 B 0 B
fcec2f2 1.58 MiB 2.12 MiB 551.50 KiB
d501a7e 0 B 0 B 0 B
7414e9b 0 B 0 B 0 B
fcec2f2 1.58 MiB 2.12 MiB 551.50 KiB
a416a65 1.58 MiB 2.12 MiB 555.26 KiB
983e0f0 0 B 0 B 0 B

Previous results on branch: feat/unhandled-sessions-protocol

Startup times

Revision Plain With Sentry Diff
0c0646f 388.59 ms 466.14 ms 77.55 ms
97f3c00 309.04 ms 359.62 ms 50.58 ms
fa28575 382.08 ms 460.50 ms 78.42 ms
4a7952f 324.85 ms 360.54 ms 35.69 ms
02a6680 341.96 ms 461.54 ms 119.58 ms

App size

Revision Plain With Sentry Diff
0c0646f 0 B 0 B 0 B
97f3c00 0 B 0 B 0 B
fa28575 0 B 0 B 0 B
4a7952f 0 B 0 B 0 B
02a6680 0 B 0 B 0 B

clone() and Session.Deserializer are both inside Session, so they can restore
the field directly. Dropping the setter keeps it off the public API surface and
makes it impossible to flip the flag on a live session without counting the
error and advancing the sequence.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sentry

sentry Bot commented Aug 10, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.52.0 (1) release

⚙️ sentry-android Build Distribution Settings

buenaflor and others added 9 commits August 10, 2026 12:10
Every other field is set at construction; the flag was the odd one out, assigned
afterwards. A private canonical constructor keeps construction complete without
putting the flag on the public API, which a 15-arg public overload would do.

Co-authored-by: Cursor <cursoragent@cursor.com>
As a bare noun phrase the field read like it held the error rather than a
boolean, most visibly where it is passed as a constructor argument.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The round-trip case duplicated one already added to SessionSerializationTest.
Keep JSON concerns in the serialization test and leave SessionTest to state
transitions.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
It was the only field in Session without the one-line comment the
surrounding declarations all carry.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
hasNonTerminatingUnhandledError is not final - recordNonTerminating
UnhandledError and update() both write it - so setting it through a
constructor established no invariant that a plain assignment does not.
Both call sites are inside Session, so clone() and the deserializer can
assign the field directly, which is what the deserializer already does
for unknown.

Removes the 15-parameter overload and the javadoc that existed to
justify it. The public constructor is unchanged, so sentry.api is too.

Co-authored-by: Cursor <cursoragent@cursor.com>
Also swaps assertFalse(serialize(...).contains(...)) for Truth's
doesNotContain, which reports the offending json on failure instead of
just "expected false".

The two new PreviousSessionFinalizerTest cases are left on Mockito
argThat, which needs a Boolean predicate rather than an assertion.

Co-authored-by: Cursor <cursoragent@cursor.com>
@buenaflor
buenaflor marked this pull request as ready for review August 11, 2026 12:19
@buenaflor
buenaflor requested a review from adinauer as a code owner August 11, 2026 12:19
Copilot AI lite review requested due to automatic review settings August 11, 2026 12:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support in the core Session model for the sessions protocol’s unhandled terminal status (for unhandled errors that do not terminate the process) and introduces an internal flag to persist this condition across serialization and recovery.

Changes:

  • Add Session.State.Unhandled and internal hasNonTerminatingUnhandledError tracking with recordNonTerminatingUnhandledError().
  • Update Session.end() to finalize flagged sessions as unhandled (while keeping crashed/abnormal precedence).
  • Add/extend tests to cover JSON round-tripping, omission when unset, and previous-session finalization behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
sentry/src/main/java/io/sentry/Session.java Adds Unhandled session state, internal non-terminating-unhandled flag, serialization/deserialization support, and end/finalization behavior.
sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt Adds coverage for round-tripping unhandled + flag and ensuring the flag is omitted when unset.
sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt Verifies previous-session finalization results in unhandled when flagged, and crashed when a native crash marker exists.
sentry/api/sentry.api Updates API dump to reflect the new enum constant and internal methods/keys.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants