Repository navigation
Phase 7: JS exceptions on both bridges, proven on device - #17
Conversation
…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.
There was a problem hiding this comment.
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),DataRequestBridgeVH 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 anAggregateErrorsplit into up to 10logExceptions) can delaylogUnhandledExceptionpast 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.mmlogUnhandledExceptionThe wrapper calls the documented Cocoa API and the iPhone log proves the call completes. beta3 still does not claim
override_report.plcrashon the next launch, so fatal JS errors on iOS never become a Bugsee crash (it.failingon 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.failingcase 10); release on WOD_LX1 retained exactly oneReactNativeWebException(controller ruling on R13).- Android ignores
labels; iOS applies them.includeVideois recorded on iOS and acted on by neither 7.x SDK.- Simulator compiles exception APIs out and never calls completion;
BGSRNSettleOnceis 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 RNErrorHandlers.js(render fatals do not steal the WeakSet via ErrorUtils).-keepnames+consumerProguardFilesmatch the plan; the release e2e asserts the FQCN incrash.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:
- Fix the Android FIFO (P2) before treating fatals under handled load as safe.
- Keep the iOS
it.failingpins; raise override-report claim to cocoa; do not mark iOS unhandled JS crashes as done.Most important issues to fix
- Unhandled must not queue behind handled on
EXCEPTION_EXECUTOR.- 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),
ReactNativeWebExceptionas a top-level class with a keep rule, iOSincludeVideo = YESoverride vs the SDK default,BGSRNSettleOnceatomic once-flag, and honestit.failingpins instead of a harness that plantslive_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 testlocally.Sent by Cursor Automation: Bugsee code review
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.
There was a problem hiding this comment.
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),DataRequestBridgeVH 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 in26266ab(ExceptionExecutors.HANDLED/UNHANDLED, plusunhandledDoesNotWaitForABlockedHandledCall).Findings
P1 High —
BugseeModule.mmlogUnhandledException(still open)The wrapper calls the documented Cocoa API and the iPhone log proves the call completes. beta3 still does not claim
override_report.plcrashon the next launch, so fatal JS errors on iOS never become a Bugsee crash (it.failingon 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.failingcase 10); release on WOD_LX1 retained exactly oneReactNativeWebException(controller ruling on R13).- Android ignores
labels; iOS applies them.includeVideois recorded on iOS and acted on by neither 7.x SDK.- Simulator compiles exception APIs out and never calls completion;
BGSRNSettleOnceis 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 RNErrorHandlers.js(render fatals do not steal the WeakSet via ErrorUtils).-keepnames+consumerProguardFilesmatch the plan; the release e2e asserts the FQCN incrash.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.failingpins. Raise override-report claim to cocoa. Do not mark iOS unhandled JS crashes as done.Most important issues to fix
- 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),ReactNativeWebExceptionas a top-level class with a keep rule, iOSincludeVideo = YESoverride vs the SDK default,BGSRNSettleOnceatomic once-flag, and honestit.failingpins instead of a harness that plantslive_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 testlocally.Sent by Cursor Automation: Bugsee code review
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.
There was a problem hiding this comment.
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.
9d7323fonly makesBugseeModule.mmhonest about beta3; production behavior is unchanged from26266ab.Prior threads
- Android FIFO (
EXCEPTION_EXECUTOR) — fully addressed.ExceptionExecutors.HANDLED/UNHANDLEDare separate single-thread executors. A handled backlog no longer spends the 1500 ms unhandled wait. Covered byunhandledDoesNotWaitForABlockedHandledCall.- iOS unhandled recovery — partially addressed. The wrapper still correctly calls
logUnhandledException:reason:completion:and storesoverride_report.plcrash. iOS SDK 7.0.0-beta30d9c9d0a-9still claims onlylive_report.plcrashon the next launch, so cases 9, 12, and iOS Release stayit.failing. The native comment no longer claims next-launch recovery. Cocoa must claimoverride_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(logUnhandledExceptionJSDoc). It still says “iOS surfaces it at the next launch.” That is false on beta3. Align it with the native comment and theit.failingpins 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.failingpins until cocoa claimsoverride_report.plcrash.CI run
36865996294was 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.
- Overall risk: Medium (High for iOS unhandled JS fatals until cocoa claims the stored report).
- Merge recommendation: Merge the Android and iOS bridges. Do not treat iOS unhandled JS exceptions as delivered.
- 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.- Positives: FIFO split is the right Android fix and is tested. R8
-keepnamesforReactNativeWebExceptionis in consumer rules.BGSRNSettleOncestill covers the simulator completion hole. The e2e harness does not fake recovery by copying crash files.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>
There was a problem hiding this comment.
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 from9d7323f. WalkedExceptionBridge/ExceptionExecutors,BugseeModuleboth sides,BGSRNExceptions/BGSRNSettleOnce, JSreportUnhandled’s 1500 ms bound, the device suite pins, and the new race test.Prior findings
- P2 Android FIFO (
ExceptionExecutors.HANDLEDvsUNHANDLED) — fully addressed in26266ab. Unhandled no longer sits behind a handled view-hierarchy wait. JVM testunhandledDoesNotWaitForABlockedHandledCallpins it.- P1 iOS unhandled recovery — still open in cocoa, not in this wrapper.
logUnhandledException:reason:completion:is the right selector; the iPhone run storesoverride_report.plcrash; beta30d9c9d0a-9still claims onlylive_report.plcrash. Cases 9, 12, and the iOS Release one-crash asserts stayit.failingon 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.
db19bd9is the right CI lock: a positive deadline was a third caller into a two-partyBGSRNSettleOnceRaceWindowbarrier, so on a slow runner the timer stole agosignal anddispatch_group_waitnever returned. Deadline0keeps that race to the twotrigger()threads;atWindowwaits 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.logUnhandledExceptioninsrc/index.tsstill says “iOS surfaces it at the next launch.” That file is not in this diff; the native comment now contradicts it.
- Overall risk: Medium (High for iOS unhandled JS fatals until cocoa claims
override_report.plcrash)- Merge recommendation: Merge the Android and iOS bridges. Do not treat iOS unhandled JS exceptions as delivered.
- Most important to fix: Cocoa must claim
override_report.plcrashthe same way aslive_report. Then un-pin cases 9, 12, and iOS Release. Optionally fix the public JSDoc so it does not promise next-launch recovery.- Positives: FIFO split is the right Android fix and is tested. R8
-keepnamesforReactNativeWebExceptionis in consumer rules.BGSRNSettleOncestill 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.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>
There was a problem hiding this comment.
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.HANDLEDvsUNHANDLED) — fully addressed in26266ab. Unhandled no longer sits behind a handled view-hierarchy wait. JVM testunhandledDoesNotWaitForABlockedHandledCallpins it. - P1 iOS unhandled recovery — still open in cocoa, not in this wrapper.
logUnhandledException:reason:completion:is the right selector; the iPhone run storesoverride_report.plcrash; beta30d9c9d0a-9still claims onlylive_report.plcrash. Cases 9, 12, and the iOS Release one-crash asserts stayit.failingon an empty recovered list. Native comment is honest since9d7323f. 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.
- Overall risk: Medium (High for iOS unhandled JS fatals until cocoa claims
override_report.plcrash) - Merge recommendation: Merge the Android and iOS bridges. Do not treat iOS unhandled JS exceptions as delivered.
- Most important to fix: Cocoa must claim
override_report.plcrashthe same way aslive_report. Then un-pin cases 9, 12, and iOS Release. Optionally fix the public JSDoc so it does not promise next-launch recovery. - Positives: FIFO split is the right Android fix and is tested. R8
-keepnamesforReactNativeWebExceptionis in consumer rules.BGSRNSettleOncestill 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.
Sent by Cursor Automation: Bugsee code review


Summary
ReactNativeWebException, with the Android class name kept through R8.it.failingbecause the SDK recovers nothing, and the harness does not plant a crash file.Test plan
fb9449b:yarn test1609 passed, mutation 95.15, JVM 207, XCTest 183 on simulator6FA9B3E8, compat 0.81.6 and 0.87.1AMRJCP4718402860: 7.5a debug and release (oneReactNativeWebException, noJavascriptExceptionBugsee bundle), 7.6b 4/4 native-crash recoveryit.failingon an empty recovered listMade with Cursor