Skip to content

feat(android): [Unhandled Sessions 3] Add internal non-terminating envelope capture - #5921

Open
buenaflor wants to merge 32 commits into
feat/unhandled-sessions-cachefrom
feat/unhandled-sessions-internal-api
Open

feat(android): [Unhandled Sessions 3] Add internal non-terminating envelope capture#5921
buenaflor wants to merge 32 commits into
feat/unhandled-sessions-cachefrom
feat/unhandled-sessions-internal-api

Conversation

@buenaflor

@buenaflor buenaflor commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

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 treat handled=false as a crash. Instead it:

  • flags the current session and increments its error count,
  • keeps the session Ok with the same session id on the scope,
  • attaches no session item and starts no new session,
  • persists the session so the flag survives process death.

The session is finalized later by normal lifecycle (endSession, background, or previous-session recovery) as unhandled, unless a terminal status takes over first, such as crashed for a native crash or abnormal for an ANR. captureEnvelope(byte[], boolean) is unchanged.

Also in this PR:

  • Scope.IWithSession becomes public so InternalSentrySdk can mutate the session under the scope lock; it is the only consumer. It carries @ApiStatus.Internal like IWithTransaction and IWithPropagationContext, so it is not supported API despite being public.
  • A shared eventStateOf returning EnvelopeEventState, replacing two loops that computed the same booleans by different routes, plus a shared readEnvelope.
  • The code added here catches Exception rather than Throwable, so OutOfMemoryError and friends propagate instead of being swallowed. The existing captureEnvelope keeps its catch (Throwable).

💡 Motivation and Context

Flutter forwards handled=false events through the terminating hybrid capture path. That marks the session crashed and may start a replacement session even though the Flutter process keeps running, incorrectly lowering crash-free session rates.

💚 How did you test it?

New InternalSentrySdkTest coverage: the session staying Ok with the same id and the flag persisted to disk; endSession afterwards finalizing as unhandled; and a later hard crash finalizing the old session as crashed and starting a fresh Ok session. Existing captureEnvelope tests confirm that path is unchanged.

📝 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.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.

🔮 Next steps

Update the Flutter Android bridge to use captureEnvelopeNonTerminating for non-terminating unhandled events.

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

@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@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 f356f78

@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@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.53.0 (1) release

⚙️ sentry-android Build Distribution Settings

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 306.53 ms 357.96 ms 51.43 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

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

@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

6 similar comments
@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@buenaflor
buenaflor force-pushed the feat/unhandled-sessions-internal-api branch from 56ab999 to 1906ec6 Compare August 11, 2026 10:00
@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

2 similar comments
@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@buenaflor
buenaflor force-pushed the feat/unhandled-sessions-internal-api branch from 06f5823 to ae23921 Compare August 11, 2026 11:53
@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@buenaflor
buenaflor force-pushed the feat/unhandled-sessions-internal-api branch from ae23921 to b2ff9d9 Compare August 11, 2026 11:54
@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@buenaflor
buenaflor force-pushed the feat/unhandled-sessions-internal-api branch from b2ff9d9 to 945602b Compare August 11, 2026 11:59
@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

Comment thread sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

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>
@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

Comment thread sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

Comment thread sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

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>
@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@runningcode runningcode 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.

This looks good aside from the exception handling!

Comment thread sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java Outdated
final SentryEnvelope repackagedEnvelope =
new SentryEnvelope(envelope.getHeader(), envelopeItems);
return scopes.captureEnvelope(repackagedEnvelope);
} catch (Throwable t) {

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.

since we arent doing disk I/O inside the try/catch, why do we need this? which parts can throw Exceptions ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

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.

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>
@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

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>
@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@buenaflor
buenaflor requested a review from runningcode August 26, 2026 13:34

@runningcode runningcode 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.

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) {

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.

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.

Comment thread sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java Outdated
Comment thread sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java Outdated
Comment thread sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java Outdated
buenaflor and others added 2 commits August 27, 2026 14:27
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>
@getsentry getsentry deleted a comment from github-actions Bot Aug 27, 2026
@getsentry getsentry deleted a comment from github-actions Bot Aug 27, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

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.

3 participants