Skip to content

fix(android): Persist hard-crash envelopes to disk before the process exits - #6937

Draft
alwx wants to merge 2 commits into
mainfrom
alwx/bugfix/6918
Draft

alwx wants to merge 2 commits into
mainfrom
alwx/bugfix/6918

Conversation

@alwx

@alwx alwx commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

When hardCrashed is true, RNSentryModuleImpl.captureEnvelope now writes the envelope to options.getEnvelopeDiskCache() on the calling thread instead of queueing it on the transport executor. It applies the same session handling as InternalSentrySdk.captureEnvelope (end the session as crashed, attach it, delete the current-session file). The next launch sends the cached file, which matches the iOS store path. Without a disk cache the old queued path runs, and if the store fails the envelope is queued instead.

💡 Motivation and Context

InternalSentrySdk.captureEnvelope only submits the envelope to the single-threaded transport executor. React Native then rethrows the fatal as JavascriptException, which is an ignored exception type, so the native SDK skips the disk flush and the process dies with the envelope still in memory. In release builds most fatal JS errors never reached Sentry.

Fixes #6918

💚 How did you test it?

Added RNSentryCaptureEnvelopeTest with five cases (sync store, crashed session attached, fallback when the store fails, no disk cache, non-fatal path). Three of them fail on main and all pass with this change. All Android unit tests and yarn lint:android pass.

📝 Checklist

  • I added tests to verify 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.
  • All tests passing.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.
  • No breaking changes.

alwx added 2 commits October 23, 2026 12:26
… exits

A fatal JS error was captured through InternalSentrySdk.captureEnvelope,
which only queues the envelope on the single-threaded transport executor.
React Native then rethrows the error as JavascriptException, an ignored
exception type, so UncaughtExceptionHandlerIntegration skips the disk
flush and the process dies with the envelope still in memory. In release
builds most fatal JS errors never reached Sentry.

When hardCrashed is set, read the envelope, end the current session as
crashed and attach it (the same session handling as
InternalSentrySdk.captureEnvelope), delete the current-session file, and
write the envelope to options.getEnvelopeDiskCache() on the calling
thread. The next launch sends the cached file, which matches the iOS
store path. Without a disk cache, or when the envelope cannot be read,
the previous queued path runs. When the store itself fails, the
repackaged envelope is queued instead so only one copy exists.

Fixes #6918
@github-actions

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

⚪ None (no version bump detected)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


  • fix(android): Persist hard-crash envelopes to disk before the process exits by alwx in #6937
  • chore(deps): update Wizard to v9.0.0 by github-actions in #6936
  • chore(deps): update CLI to v3.8.2 by github-actions in #6933
  • ci: Move CI off end-of-life Node versions to Node 22 by antonis in #6927

🤖 This preview updates automatically when you update the PR.

@github-actions

Copy link
Copy Markdown
Contributor
Fails
🚫 Pull request is not ready for merge, please add the "ready-to-merge" label to the pull request

Generated by 🚫 dangerJS against dc4fb97

Comment on lines +703 to +711
if (!EnvelopeCache.getCurrentSessionFile(cacheDirPath).delete()) {
logger.log(SentryLevel.WARNING, "Failed to delete the current session file.");
}
}

final @NotNull SentryEnvelope repackaged = new SentryEnvelope(envelope.getHeader(), items);
if (!cache.storeEnvelope(repackaged, new Hint())) {
logger.log(SentryLevel.ERROR, "Failed to store the hard crash envelope, queueing it.");
scopes.captureEnvelope(repackaged);

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.

Current session file is deleted even when hard-crash envelope persistence fails

The hard-crash path deletes the current-session file before storing the repackaged envelope. If cache.storeEnvelope fails, the envelope is only queued (or a thrown exception is caught by the outer handler), while the session file is already gone. Preserve the session file unless the envelope is successfully stored to disk, so the next launch can still report the session as abnormal.

Evidence
  • The session is attached to items, then its current-session file is deleted before cache.storeEnvelope is called.
  • When storeEnvelope returns false, the code queues the envelope through scopes.captureEnvelope; the enclosing capture path can then exit while the process is about to terminate.
  • The existing store-failure test checks queue fallback but does not cover a session file surviving a failed store.

Identified by Warden · find-bugs · 638-86W

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.

[Android] Fatal JS errors lost in release builds: hard-crash envelope is only queued, then JavascriptException is dropped without a flush

1 participant