Skip to content

feat(ios): Report JS thread hangs as errors (iOS only) - #6964

Merged
alwx merged 14 commits into
mainfrom
alwx/feature/3460
Nov 5, 2026
Merged

alwx merged 14 commits into
mainfrom
alwx/feature/3460

Conversation

@alwx

@alwx alwx commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📢 Type of change

  • New feature

📜 Description

Adds the experimental, iOS-only enableJsHangTracking and jsHangTimeoutInterval options. When the JS thread does not run a watchdog task within the timeout, the SDK captures a JS Thread Hang error event with the Hermes JS stack and the bundle Debug ID. The tracker does not run in __DEV__ builds, and during a profile it reads the profile samples and puts them back into the profile.

💡 Motivation and Context

App Hang tracking watches only the UI thread, so an infinite loop in JS sent no event. Closes #3460.

💚 How did you test it?

Before the review fixes, on the iOS 26.1 simulator (New Architecture), a 5 s block and an infinite loop each produced one event with the blocking function as the top frame, and the Release build resolved it to the right source line. The review fixes are covered by Jest tests and by a local test with a stub Hermes for the stack choice, the profile merge, and a stop call during a sample.

After the review fixes, @antonis tested the latest commit:

📝 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.

🔮 Next steps

Android is left out because reload crashes in the Hermes sampler thread (meta/hermes#1853), and the follow-up is #6985. Still to do for iOS: native unit tests in a follow-up PR, and the sentry-docs page.

@github-actions

github-actions Bot commented Oct 29, 2026 •

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


  • feat(ios): Report JS thread hangs as errors (iOS only) by alwx in #6964
  • feat(ios): Report JS thread hangs as errors (iOS only) by alwx in #6964
  • chore(deps): update Cocoa SDK to v9.33.0 by github-actions in #6987
  • feat(profiling): Send Hermes samples as UI profile chunks on Android by alwx in #6966
  • fix(tracing): Detect expo/fetch behind the Expo Router fetch wrapper by antonis and claude in #6965
  • ci(release): Bump craft to 3.9.0 and update config by BYK with sentry-junior in #6984
  • docs(core): Drop stale logger.warn note from AGENTS.md by antonis and claude in #6968
  • chore(deps): update JavaScript SDK to v10.78.0 by antonis and claude in #6973
  • chore(deps): Bump getsentry/craft from 2.35.0 to 3.5.0 by alwx in #6983
  • chore(deps): update Maestro to v2.12.1 by github-actions in #6981
  • chore(deps): bump getsentry/craft/.github/workflows/changelog-preview.yml from 3.0.0 to 3.4.1 by alwx with dependabot in #6979
  • chore(deps): bump node-forge from 1.4.0 to 1.4.2 by dependabot in #6982
  • chore(deps): bump getsentry/github-workflows/validate-pr from 36c729264d2edc29ebae61950c50e1e9f043ad7e to 01bcb7fe69bb48bacd83e675840e3dc95b33c730 by dependabot in #6978
  • chore(deps): bump ruby/setup-ruby from 1.334.0 to 1.336.0 by dependabot in #6977
  • chore(deps): bump gradle/actions/setup-gradle from 6.4.0 to 6.4.1 by dependabot in #6976
  • fix(ios): Honor Sentry.Mask in error screenshots by antonis and claude in #6974
  • fix(android): Restore enableLogs option mapping by antonis and claude in #6967
  • chore(deps): update Wizard to v9.1.0 by github-actions in #6972
  • chore(deps): update Sentry Android Gradle Plugin to v6.25.0 by github-actions in #6971

🤖 This preview updates automatically when you update the PR.

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor
Fails
🚫 Please consider adding a changelog entry for the next release.
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Instructions and example for changelog

Please add an entry to CHANGELOG.md to the "Unreleased" section. Make sure the entry includes this PR's number.

Example:

## Unreleased

### Features

- Report JS thread hangs as errors (iOS only) ([#6964](https://github.com/getsentry/sentry-react-native/pull/6964))

If none of the above apply, you can opt out of this check by adding #skip-changelog to the PR description or adding a skip-changelog label.

Generated by 🚫 dangerJS against eb24c3b

@alwx
alwx force-pushed the alwx/feature/3460 branch from 0f9e51b to 49a9e7b Compare November 3, 2026 13:14
@alwx alwx changed the title feat(ios): Report JS thread hangs as errors (prototype) feat(core): Report JS thread hangs as errors Nov 3, 2026
Comment thread packages/core/ios/RNSentry.mm Resolved
Comment thread packages/core/src/js/wrapper.ts Resolved
@alwx alwx changed the title feat(core): Report JS thread hangs as errors feat(ios): Report JS thread hangs as errors Nov 4, 2026
@alwx
alwx marked this pull request as ready for review November 4, 2026 07:55
@alwx
alwx requested review from a team and lucas-zimerman as code owners November 4, 2026 07:55
@alwx
alwx requested review from 0xadam-brown, antonis, buenaflor, itaybre, philprime and runningcode and removed request for a team November 4, 2026 07:55
@alwx alwx changed the title feat(ios): Report JS thread hangs as errors feat(ios): Report JS thread hangs as errors (iOS only) Nov 4, 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.

Stale Bugbot comment from a previous run.

@antonis antonis added the ready-to-merge Triggers the full CI test suite label Nov 4, 2026
@alwx
alwx force-pushed the alwx/feature/3460 branch from b65bad6 to f8f9d1c Compare November 4, 2026 11:06
@sentry

sentry Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
Sentry RN io.sentry.reactnative.sample 8.33.0 (113) Release

⚙️ sentry-react-native Build Distribution Settings

@alwx
alwx requested a review from antonis November 4, 2026 11:31
Comment thread packages/core/ios/RNSentry.mm Resolved
RNSentry runs on the main queue, so stop() blocked the UI thread while it waited for a running Hermes sample. The sample slept for its whole 300 ms window. When the wait timed out, a worker that still waited for the sampler lock could also turn the sampler on after stop() returned.

The sample now waits on the tracker's wake-up semaphore instead of sleeping. stop() signals that semaphore, so a running sample ends at once and turns the sampler off. A sample that starts later sees the stored signal and ends at once too. The 1 s limit stays only as protection against a deadlock.

invalidate now stops the tracker before it calls super, so the sampler is off before the module tears down.
@alwx

alwx commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

About the new Warden findings:

  • stop() can block the main method queue: Fixed
  • stop() can return while a Hermes sample is still running: Fixed
  • invalidate can proceed while Hermes hang sampling is still running: Fixed
  • JS hang stop/start blocks the main method queue: Fixed

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

Thank you for iterating on this @alwx 🙇
Did a few checks (example 1, example 2) and added a couple of comments. Overall the feature looks good.

Let's also add some links in the description

Comment thread packages/core/ios/RNSentry.mm Resolved
@lucas-zimerman

Copy link
Copy Markdown
Collaborator

The PR description might be a bit outdated, it mentions the changelog will be updated on the next step but it was added on this PR

alwx added 3 commits November 5, 2026 08:34
A very short jsHangTimeoutInterval sent an event and took a 300 ms Hermes sample on every short stall. The integration now uses at least 0.25 seconds. A value that is not a finite number, like NaN, uses the default of 2 seconds. Both cases log a warning.

The native check rejected only values <= 0, so NaN reached the uint64_t cast, which is undefined behavior. The check is now negated, so it rejects NaN too.
When a Hermes profile ran, the hang sample returned nothing, so the hang event had no stack. The sample app profiles every transaction, so this was the usual case. The tracker skipped the sample on purpose: Hermes clears its samples on every dump, so a dump would remove the samples that the profile collected before the hang.

During a profile the sampler is already on and its newest samples show the hang. The tracker now dumps them at once and takes the stack from the newest JS sample. The sampler keeps the dumped traces and merges them into the profile in stopHermesProfiling. Every Hermes trace numbers its stack frames from 1, so the merge moves the frame ids and parents of each trace past the ids before it. Samples stay in time order, so the JS profile conversion does not change. A new profile drops the traces of the profile before it.
startJsHangTracking casts the timeout to uint64_t. A value below 1 ms became 0, so the tracker reported a hang for every ping that was still pending at the next poll. Infinity passed the check too, and casting it is undefined behavior, like NaN. The method now rejects values that are not finite or are below 1 ms. The JS integration already sends at least 250 ms, so this only guards the bridge.
@alwx

alwx commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

About the Warden finding JS hang timeout accepts non-finite and sub-millisecond values: Fixed in the latest commits

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

Reviewed by Cursor Bugbot for commit 2dd6b4d. Configure here.

alwx added 2 commits November 5, 2026 09:53
A JS debugger that pauses at a breakpoint blocks the JS thread, so the tracker reported every pause longer than the timeout as a hang. The native check for an attached debugger sees only native tracers like lldb, not the Hermes inspector, and native code has no flag for a paused JS debugger.

The integration now does not start the tracker when __DEV__ is true. sentry-android does the same for ANRs, which it does not report in debug builds by default. The option docs and the CHANGELOG entry say so.
stop() waited at most 1 s for the tracker thread and then returned, even when the thread still dumped the Hermes sampler. invalidate could then tear down the JS runtime while the sampler was in use, and a restart could start a new thread next to the old one.

stop() now waits until the thread ends. This cannot deadlock: the thread never waits for the main thread or for the tracker lock, and the only lock it takes is the sampler mutex, which is held only for a dump. To keep the wait short, the thread now only takes the sample. Parsing the trace, building the event, and the capture run on a background queue. In a test with a stub Hermes, stop() during a sample window took 0.3 ms, and the event was still captured with its stack.
@alwx

alwx commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

About the stop() findings in the Warden run (block the main thread / return before sampling finishes / return while the thread still runs): Fixed in eb24c3b.

stop() now waits until the tracker thread ends, without a time limit. I wrote earlier that an unbounded wait could deadlock, but it cannot: the thread never waits for the main thread or for the tracker lock, and the sampler mutex is held only for a dump. The thread now only takes the sample, and parsing, the event, and the capture run on a background queue, so the wait stays short.

Comment thread packages/core/ios/RNSentry.mm Resolved

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

Thank you for iterating on this @alwx 🙇

LGTM 👍 Did a couple of tests locally and it worked as expected:

🔮 Next steps
Android is left out because reload crashes in the Hermes sampler thread (meta/hermes#1853), and the follow-up is #6985. Still to do for iOS: native unit tests, a symbolication check on sentry.io, and the sentry-docs page.

  • Let's also update the PR description and add a link.
  • I'm fine with iterating on a next PR with the missing unit tests.

@alwx
alwx merged commit 2dfebe7 into main Nov 5, 2026
149 of 156 checks passed
@alwx
alwx deleted the alwx/feature/3460 branch November 5, 2026 10:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge Triggers the full CI test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Report JS Loop stalls as Errors (similar to App Hangs on iOS)

3 participants