Skip to content

Phase 7: JS exceptions on both bridges, proven on device - #17

Merged
krassx merged 13 commits into
mainfrom
feat/phase-7-5b
Oct 1, 2026
Merged

krassx merged 13 commits into
mainfrom
feat/phase-7-5b

Conversation

@krassx

@krassx krassx commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Android and iOS report a JS exception as one ReactNativeWebException, with the Android class name kept through R8.
  • Device suites cover handled, unhandled, and render exceptions. On Android 7.3.0 a release fatal retains exactly one Bugsee crash of that class. On iOS 7.0.0-beta3 the desired one-crash recovery is pinned as it.failing because the SDK recovers nothing, and the harness does not plant a crash file.
  • The plan records the Phase 7 gate and the completed Phase 8 tasks that follow on their own branches.

Test plan

  • Phase 7 gate on fb9449b: yarn test 1609 passed, mutation 95.15, JVM 207, XCTest 183 on simulator 6FA9B3E8, compat 0.81.6 and 0.87.1
  • WOD_LX1 AMRJCP4718402860: 7.5a debug and release (one ReactNativeWebException, no JavascriptException Bugsee bundle), 7.6b 4/4 native-crash recovery
  • iPhone XS (KRSFT): 7.5b plain cases passed; cases 9, 12, and the Release one-crash assertion are it.failing on an empty recovered list
  • Merge the stacked Phase 8 PRs after this one, in order: 8.1, then 8.2a, then 8.2b and 8.2c

Made with Cursor

krassx and others added 9 commits September 30, 2026 22:07
…through R8

Co-authored-by: Cursor <cursoragent@cursor.com>
…rom render

Banner: Bugsee Android SDK 7.3.0 [beb390dc0].

One handled payload (nonce 6877ef246252):
{"name":"TypeError","reason":"E2E handled 6877ef246252","frames":[{"data":{"member":"bugseeE2EThrowSite"},"debug_id":"8a1c2f4e-0d3b-5e6f-9a7b-6877ef246252"}],"cause":{"name":"RangeError","reason":"inner 6877ef246252"},"signature":"8362dd3b5785297d7d387925b94ce0ea5e857cfc","platform_os":"android","debug_ids":{"http://localhost:8081/index.bundle//&platform=android&dev=true&lazy=true&minify=false&app=com.bareexample&modulesOnly=false&runModule=true&excludeSource=true&sourcePaths=url-server":"8a1c2f4e-0d3b-5e6f-9a7b-6877ef246252"}}

Gated release R1–R3 bundles (nonce 824ac61adb58): exactly one crash,
com.bugsee.reactnative.ReactNativeWebException, reason JSON with
"E2E fatal 824ac61adb58". No JavascriptException Bugsee bundle observed
(R2 prediction unmet; assertion left intact for controller ruling).

Thread: exception handled sent tid→comm BugseeRN-except (TASK_COMM_LEN
truncation of BugseeRN-exceptions).

Co-authored-by: Cursor <cursoragent@cursor.com>
Controller ruling: R13's second Bugsee crash is not retained on Android
SDK 7.3.0. Logcat still shows FATAL EXCEPTION / JavascriptException and
the process dies; Bugsee keeps exactly one crash named
ReactNativeWebException. Gated R2/R3 updated; debug case 10 stays
it.failing. awaitBundles(1) for the release beforeAll.

Release verify (WOD_LX1): R1–R3 PASS; one bundle
894ba5df-af93-4d4b-9678-5d06ec218db4.bundle.zip.

Co-authored-by: Cursor <cursoragent@cursor.com>
ExceptionStage registers _bugseeDebugIds synchronously before mounting
BugseeE2EThrower, so the map exists for the first render throw. Root
polls for a crash with the boundary reason instead of requiring two
bundles. Release R1 asserts exactly one ours.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…cannot be lost

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
beta3 0d9c9d0a-9 stores the unhandled report and does not claim override_report.plcrash on the next launch. The one-crash asserts stay it.failing so an empty recovery list cannot pass as the desired outcome.

Co-authored-by: Cursor <cursoragent@cursor.com>
The device runs changed two predictions, and the boxes were still open.

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

Deep code review — PR #17 (head 9699234)

Phase 7 native bridges: JS exceptions cross as ReactNativeWebException (Android class kept through R8; iOS name constant), with device coverage for handled / unhandled / render. Reviewed against the JS fatal path (handlers.ts / report.ts / RootErrorReporter), DataRequestBridge VH deadlines, the 7.1c/7.1d plan, and the 7.5a/7.5b e2e — not the diff alone.

Findings

P2 Medium — BugseeModule.java (EXCEPTION_EXECUTOR)

Handled and unhandled share one FIFO thread that the SDK blocks for view hierarchy (~450 ms). JS only waits 1500 ms (UNHANDLED_REPORT_WAIT_MS) before RN’s handler runs and, in release, kills the process. A handled backlog (including an AggregateError split into up to 10 logExceptions) can delay logUnhandledException past that budget so the crash is never stored. The fatal e2e starts from an idle queue, so it does not see this. Fix: unhandled must not sit behind handled on this executor.

P1 High — BugseeModule.mm logUnhandledException

The wrapper calls the documented Cocoa API and the iPhone log proves the call completes. beta3 still does not claim override_report.plcrash on the next launch, so fatal JS errors on iOS never become a Bugsee crash (it.failing on cases 9, 12, and iOS Release). This is not a wrong selector; it is the pinned SDK. Do not treat iOS unhandled as delivered until cocoa claims that file. Do not copy it in the harness.

Also checked, not reported as wrapper defects

  • Android 7.3.0 debug fatals file a crash and an error (it.failing case 10); release on WOD_LX1 retained exactly one ReactNativeWebException (controller ruling on R13).
  • Android ignores labels; iOS applies them. includeVideo is recorded on iOS and acted on by neither 7.x SDK.
  • Simulator compiles exception APIs out and never calls completion; BGSRNSettleOnce is the right defense and the concurrent test actually uses an atomic.
  • Payload is forwarded verbatim (no native parse/log/truncate). markReported / previous-handler-once / 1500 ms JS cap are unchanged from PR 16 and still look correct relative to RN ErrorHandlers.js (render fatals do not steal the WeakSet via ErrorUtils).
  • -keepnames + consumerProguardFiles match the plan; the release e2e asserts the FQCN in crash.json.

Overall risk: Medium

High for iOS unhandled JS fatals until the SDK claims override reports. Android handled, unhandled (idle queue), and R8, and iOS handled / rejection / boundary, are in good shape on the device evidence this PR cites.

Merge recommendation

Merge the bridges, but:

  1. Fix the Android FIFO (P2) before treating fatals under handled load as safe.
  2. Keep the iOS it.failing pins; raise override-report claim to cocoa; do not mark iOS unhandled JS crashes as done.

Most important issues to fix

  1. Unhandled must not queue behind handled on EXCEPTION_EXECUTOR.
  2. iOS unhandled recovery is an SDK hole, not a wrapper follow-up to “just land”.

Positive

Dedicated Android exception thread (so VH can still be answered), ReactNativeWebException as a top-level class with a keep rule, iOS includeVideo = YES override vs the SDK default, BGSRNSettleOnce atomic once-flag, and honest it.failing pins instead of a harness that plants live_report.plcrash.

CI at review: android, ios unit / CocoaPods / SPM, rn-compat 0.81/0.83/0.86/0.87 pass. lint+mutation and ios e2e simulator were still pending. I did not run yarn test locally.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread packages/react-native/ios/BugseeModule.mm
JS waits 1500 ms for a fatal report. A handled backlog on the same thread can spend that wait inside the view-hierarchy call, so the crash is never stored.

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

Deep code review — PR #17 (head 26266ab)

Phase 7 native bridges: JS exceptions cross as ReactNativeWebException (Android class kept through R8; iOS name constant), with device coverage for handled / unhandled / render. Reviewed against the JS fatal path (handlers.ts / report.ts / RootErrorReporter), DataRequestBridge VH deadlines, the 7.1c/7.1d plan, and the 7.5a/7.5b e2e — not the diff alone.

Prior finding on 9699234: handled and unhandled sharing one FIFO executor. Fixed in 26266ab (ExceptionExecutors.HANDLED / UNHANDLED, plus unhandledDoesNotWaitForABlockedHandledCall).

Findings

P1 High — BugseeModule.mm logUnhandledException (still open)

The wrapper calls the documented Cocoa API and the iPhone log proves the call completes. beta3 still does not claim override_report.plcrash on the next launch, so fatal JS errors on iOS never become a Bugsee crash (it.failing on cases 9, 12, and iOS Release). The method comment still says the SDK surfaces this at the next launch; that is false on this pin. Do not treat iOS unhandled as delivered, and do not copy the file in the harness.

Also checked, not reported as wrapper defects

  • Android 7.3.0 debug fatals file a crash and an error (it.failing case 10); release on WOD_LX1 retained exactly one ReactNativeWebException (controller ruling on R13).
  • Android ignores labels; iOS applies them. includeVideo is recorded on iOS and acted on by neither 7.x SDK.
  • Simulator compiles exception APIs out and never calls completion; BGSRNSettleOnce is the right defense and the concurrent test actually uses an atomic.
  • Payload is forwarded verbatim (no native parse/log/truncate). markReported / previous-handler-once / 1500 ms JS cap are unchanged from PR 16 and still look correct relative to RN ErrorHandlers.js (render fatals do not steal the WeakSet via ErrorUtils).
  • -keepnames + consumerProguardFiles match the plan; the release e2e asserts the FQCN in crash.json.
  • iOS VH is async off main (unlike Android, which blocks the caller ~450 ms). Putting iOS exception calls on main does not recreate the Android queue bug.

Overall risk: Medium

High for iOS unhandled JS fatals until the SDK claims override reports. Android handled, unhandled (including behind a handled backlog at the wrapper queue), and R8, and iOS handled / rejection / boundary, are in good shape on the device evidence this PR cites.

Merge recommendation

Merge the bridges, but keep the iOS it.failing pins. Raise override-report claim to cocoa. Do not mark iOS unhandled JS crashes as done.

Most important issues to fix

  1. iOS unhandled recovery is an SDK hole, not a wrapper follow-up to “just land”.

Positive

The Android FIFO split is the right shape: unhandled no longer sits behind handled on BugseeRN-exceptions, and the JVM test actually blocks handled while unhandled must start. Dedicated exception threads (so VH can still be answered), ReactNativeWebException as a top-level class with a keep rule, iOS includeVideo = YES override vs the SDK default, BGSRNSettleOnce atomic once-flag, and honest it.failing pins instead of a harness that plants live_report.plcrash.

CI at review: ios unit / CocoaPods / SPM, rn-compat 0.81/0.83/0.86/0.87 pass. android, lint+mutation, and ios e2e simulator were still pending. I did not run yarn test locally.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread packages/react-native/ios/BugseeModule.mm
The method comment said the SDK surfaces the stored report on the next launch. On 7.0.0-beta3 it claims live_report.plcrash and leaves override_report.plcrash unread.

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

Deep review (head 9d7323f)

Reviewed the bridges against callers, SDK contracts, JVM/XCTest, and the device e2e pins — not the latest comment-only commit in isolation. 9d7323f only makes BugseeModule.mm honest about beta3; production behavior is unchanged from 26266ab.

Prior threads

  • Android FIFO (EXCEPTION_EXECUTOR) — fully addressed. ExceptionExecutors.HANDLED / UNHANDLED are separate single-thread executors. A handled backlog no longer spends the 1500 ms unhandled wait. Covered by unhandledDoesNotWaitForABlockedHandledCall.
  • iOS unhandled recovery — partially addressed. The wrapper still correctly calls logUnhandledException:reason:completion: and stores override_report.plcrash. iOS SDK 7.0.0-beta3 0d9c9d0a-9 still claims only live_report.plcrash on the next launch, so cases 9, 12, and iOS Release stay it.failing. The native comment no longer claims next-launch recovery. Cocoa must claim override_report; this wrapper must not copy one file onto the other.
  • Stale native comment — fully addressed by 9d7323f.

New wrapper findings

No new P0–P3 bugs in this wrapper that warrant inline comments.

Residual (not in this diff, still wrong for callers of the public API):

  • P3 — packages/react-native/src/index.ts (logUnhandledException JSDoc). It still says “iOS surfaces it at the next launch.” That is false on beta3. Align it with the native comment and the it.failing pins so apps do not plan on a recovery that will not happen.

Residual product hole (cocoa, not this PR)

Unhandled JS fatals on iOS are stored and then dropped. Android unhandled JS fatals on this branch are the delivered path. Keep the honest it.failing pins until cocoa claims override_report.plcrash.

CI run 36865996294 was still queued at review time (yarn test/lint/typecheck and Android unit passed on earlier heads of this PR). I did not re-run the suite in this pass.


  1. Overall risk: Medium (High for iOS unhandled JS fatals until cocoa claims the stored report).
  2. Merge recommendation: Merge the Android and iOS bridges. Do not treat iOS unhandled JS exceptions as delivered.
  3. Most important to fix: Cocoa must claim override_report.plcrash. Then un-pin cases 9, 12, and iOS Release. Optionally fix the public JSDoc so it does not promise next-launch recovery.
  4. Positives: FIFO split is the right Android fix and is tested. R8 -keepnames for ReactNativeWebException is in consumer rules. BGSRNSettleOnce still covers the simulator completion hole. The e2e harness does not fake recovery by copying crash files.
Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

The deadline is a third caller into that barrier. On a slow CI runner it fires while the two threads are parked and steals a signal, so the test never finishes.

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

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

Deep review (db19bd9)

Re-review of the Phase 7 bridges after db19bd9 (test(ios): keep the settle-once race to the two callers). Production Android/iOS exception paths are unchanged from 9d7323f. Walked ExceptionBridge / ExceptionExecutors, BugseeModule both sides, BGSRNExceptions / BGSRNSettleOnce, JS reportUnhandled’s 1500 ms bound, the device suite pins, and the new race test.

Prior findings

  • P2 Android FIFO (ExceptionExecutors.HANDLED vs UNHANDLED) — fully addressed in 26266ab. Unhandled no longer sits behind a handled view-hierarchy wait. JVM test unhandledDoesNotWaitForABlockedHandledCall pins it.
  • P1 iOS unhandled recovery — still open in cocoa, not in this wrapper. logUnhandledException:reason:completion: is the right selector; the iPhone run stores override_report.plcrash; beta3 0d9c9d0a-9 still claims only live_report.plcrash. Cases 9, 12, and the iOS Release one-crash asserts stay it.failing on an empty recovered list. Native comment is honest. Harness still does not copy one file onto the other.
  • Stale native comment — fully addressed in 9d7323f.

New findings

No new P0–P3 on the wrapper. db19bd9 is the right CI lock: a positive deadline was a third caller into a two-party BGSRNSettleOnceRaceWindow barrier, so on a slow runner the timer stole a go signal and dispatch_group_wait never returned. Deadline 0 keeps that race to the two trigger() threads; atWindow waits are now bounded (5 s) so a stuck handshake fails the test instead of killing the job.

Residual (not a wrapper defect): public JSDoc on Bugsee.logUnhandledException in src/index.ts still says “iOS surfaces it at the next launch.” That file is not in this diff; the native comment now contradicts it.


  1. Overall risk: Medium (High for iOS unhandled JS fatals until cocoa claims override_report.plcrash)
  2. Merge recommendation: Merge the Android and iOS bridges. Do not treat iOS unhandled JS exceptions as delivered.
  3. Most important to fix: Cocoa must claim override_report.plcrash the same way as live_report. Then un-pin cases 9, 12, and iOS Release. Optionally fix the public JSDoc so it does not promise next-launch recovery.
  4. Positives: FIFO split is the right Android fix and is tested. R8 -keepnames for ReactNativeWebException is in consumer rules. BGSRNSettleOnce still covers the simulator completion hole, and the concurrent-settle test no longer deadlocks CI. The e2e harness does not fake recovery by copying crash files.
Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

A shared go semaphore let the faster caller enter the next window and consume the other caller's release, so that caller never arrived and CI timed out.

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

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

Deep review (17c7f6b)

Re-review of the Phase 7 bridges after 17c7f6b (test(ios): keep each settle-once window on its own release). Walked the production callers, not just the latest test commit: ExceptionBridge / ExceptionExecutors / Android BugseeModule.logUnhandledException, iOS BugseeModule + BGSRNExceptions / BGSRNSettleOnce, JS reportUnhandled’s 1500 ms bound and RootErrorReporter, R8 -keepnames, and the device suite pins. Production Android/iOS exception paths are unchanged from 9d7323f / 26266ab.

Prior findings

  • P2 Android FIFO (ExceptionExecutors.HANDLED vs UNHANDLED) — fully addressed in 26266ab. Unhandled no longer sits behind a handled view-hierarchy wait. JVM test unhandledDoesNotWaitForABlockedHandledCall pins it.
  • P1 iOS unhandled recovery — still open in cocoa, not in this wrapper. logUnhandledException:reason:completion: is the right selector; the iPhone run stores override_report.plcrash; beta3 0d9c9d0a-9 still claims only live_report.plcrash. Cases 9, 12, and the iOS Release one-crash asserts stay it.failing on an empty recovered list. Native comment is honest since 9d7323f. Harness still does not copy one file onto the other.
  • Stale native comment — fully addressed in 9d7323f.

New findings

No new P0–P3 on the wrapper. 17c7f6b is the right lock for the remaining ios-unit flake: a shared go let the faster caller leave window 0, enter window 1, and consume the other thread’s release, so that caller never reached window 1 (dispatch_group_wait then hung until the job was killed). Each BGSRNSettleOnceRaceWindow phase now has its own entered/release pair and a fresh 5 s wait. Deadline remains 0 so a timer cannot join the two-party barrier.

Residual (not a wrapper defect, not in this diff): public JSDoc on Bugsee.logUnhandledException in src/index.ts still says “iOS surfaces it at the next launch.” The native comment now contradicts it.

CI run 36896973758 was still queued at review time (the previous run was cancelled by this push). I did not run yarn test, JVM, or XCTest in this pass.


  1. Overall risk: Medium (High for iOS unhandled JS fatals until cocoa claims override_report.plcrash)
  2. Merge recommendation: Merge the Android and iOS bridges. Do not treat iOS unhandled JS exceptions as delivered.
  3. Most important to fix: Cocoa must claim override_report.plcrash the same way as live_report. Then un-pin cases 9, 12, and iOS Release. Optionally fix the public JSDoc so it does not promise next-launch recovery.
  4. Positives: FIFO split is the right Android fix and is tested. R8 -keepnames for ReactNativeWebException is in consumer rules. BGSRNSettleOnce still covers the simulator completion hole, and the concurrent-settle test no longer deadlocks CI on a stolen phase-1 signal. The e2e harness does not fake recovery by copying crash files.
Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@krassx
krassx merged commit 57af056 into main Oct 1, 2026
12 checks passed
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