Skip to content

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

Draft
buenaflor wants to merge 6 commits into
feat/unhandled-sessionsfrom
feat/unhandled-sessions-protocol
Draft

feat(core): [Unhandled Sessions 1] Add Unhandled session state and non-terminating error flag#5919
buenaflor wants to merge 6 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 unhandled session status to the session model.

  • Session.State.Unhandled — the protocol status for an unhandled error that did not terminate the process.
  • A nonTerminatingUnhandledError flag on Session (non_terminating_unhandled_error when serialized) that records "this session saw an unhandled error the process survived" without ending the session or changing its status while it is still alive.
  • end() finalizes a session carrying the flag as Unhandled instead of Exited. Crashed and Abnormal keep taking precedence, and update(Crashed, ...) clears the flag so a real crash always wins.
  • clone() and the (de)serializer carry the flag; it is omitted from JSON when unset.

Nothing sets the flag yet — this PR is inert on its own. It is the model layer for the rest of the stack.

A note on the name: "unhandled" alone is ambiguous, since a native crash is also an unhandled error — it just terminates the process and so ends the session as crashed rather than unhandled. The flag is therefore named after the property that actually distinguishes the two, matching the vocabulary of captureEnvelopeNonTerminating later in the stack.

💡 Motivation and Context

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

The session protocol has had a dedicated status for exactly this case since 1.6.0: unhandled — "an unhandled error occurred but the process did not terminate". Relay accepts it (SessionStatus::Unhandled"unhandled").

💚 How did you test it?

New SessionTest covering the flag's atomic update, terminal-state precedence (Crashed/Abnormal win), clone(), and serialization round-trips. PreviousSessionFinalizerTest covers recovery of a previous session carrying the flag, including a native crash escalating it to crashed. SessionSerializationTest covers the JSON round-trip.

📝 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

Persisting the flag across process death and exposing the capture API, in the following PRs in this stack.

#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 407c39d

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 388.59 ms 466.14 ms 77.55 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
02a6680 341.96 ms 461.54 ms 119.58 ms
97f3c00 309.04 ms 359.62 ms 50.58 ms
4a7952f 324.85 ms 360.54 ms 35.69 ms

App size

Revision Plain With Sentry Diff
02a6680 0 B 0 B 0 B
97f3c00 0 B 0 B 0 B
4a7952f 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 3 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>
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.

1 participant