Repository navigation
[Due for payment 2026-09-09] [Sentry: APP-4GM] HybridApp iOS OldDot SIGABRT — NSNull force cast to NSNumber in GuidedReview control banner #99853
Description
Activity
- added a parent issue
on Aug 30, 2026 - addedImprovementItem broken or needs improvement.Item broken or needs improvement.
on Aug 31, 2026 @MelvinBot investigate this
Reacted by melvin-botIssue Analysis
This crash cannot be fixed in
Expensify/App. Every frame below the JavaScriptCore bridge is OldDot Swift that lives inMobile-Expensify. App has no code path, native module method, config flag, or patch that reachessetupControlBanner— and Sentry'snd.js_started: falseconfirms it fires before NewDot's JS runtime even starts. Route this to someone withMobile-Expensifyaccess.Root cause: the description's hypothesis holds. YAPL runs in JavaScriptCore and calls
setupControlBanneracross the ObjC bridge. A JSnullin that argument payload bridges toNSNull, andsetupControlBannerforce-casts it to a number.swift_dynamicCastFailure→SIGABRT. It happens insideviewDidLoadwhileUISheetPresentationControlleris beginning its transition, so there is no recovery point — the process is gone before anything is on screen. That matches the profile:level: fatal,mechanism: signal,uptime_bucket: 0-30s, and the same users crashing repeatedly on cold start whenever Guided Review is the landing surface.Two traps for whoever picks this up:
- Don't only grep for
as! NSNumber. Swift bridgesas! Bool,as! Int, andas! DoublefromAny/AnyObjectthroughNSNumber, so all of those produce this exact message. The offending line may not mentionNSNumberat all. NSNullis notnil.if let x = dict["key"]andguard letboth succeed onNSNulland then trap on the cast. The guard has to beas? NSNumberwith a default, or an explicitis NSNullcheck.
Fix both sides, as the description says: a conditional cast with a sensible default in
setupControlBanner(stops the abort), plus removing thenullfrom the YAPL payload (fixes the data). The Swift guard is the one that ships the crash fix.Investigation details
Verified App has nothing to do with this:
- A case-insensitive grep for
guidedreview|guided review|controlBanner|control_banneracross the whole App checkout matches only two unrelated help-site markdown articles underdocs/articles/expensify-classic/. Zero code hits. - YAPL appears in App only as a comment in
src/libs/Notification/PushNotification/index.native.ts:88and as an opaque native startup-marker name intests/unit/telemetryStartupMarkersTest.ts. No implementation. - The full App↔OldDot bridge surface is
modules/hybrid-app/src/types.ts—isHybridApp,signInToOldDot,switchAccount,completeOnboarding, and friends. Nothing there constructs, gates, or presents an OldDot view controller. - App's HybridApp orchestration (
HybridAppHandler.tsx,libs/HybridApp.ts) runs only aftergetHybridAppSettings()resolves and Onyx session data loads. That is strictly after the JS runtime starts, and this crash reportsnd.js_started: false.
Alternative explanations considered and rejected:
- A NewDot-side field being written as null. Ruled out — the crash precedes JS startup, so no NewDot code has run to write anything.
- A React Native / Fabric bridge crash. Ruled out — the stack is
YAPLJS→YAPLEnhancedCommandHandler→ Swift view controller, which is the OldDot JavaScriptCore bridge, not the RN bridge. TheHybridAppmentions inpatches/*/details.mdare unrelated RN teardown crashes.
What I could not confirm: the exact field name and source line.
Mobile-Expensifyis a private submodule that isn't checked out here, and I have no Sentry access in this run, so I can't readsetupControlBanneror pull the event payload to name the null key. Someone with repo access should read the function directly; the Sentry event's YAPL args will name the field.Reproduction note: this is an iOS HybridApp OldDot native crash, so it can't be reproduced or verified in a browser. It needs a device or simulator running HybridApp on an account whose Guided Review banner field comes back null.
Related issues: only the two already linked in the description — #98103 and the #91302 tracker. No other duplicates found.
Precedent for routing: #98103 is the same Guided Review surface with the same shape of bug (a null/undefined field from the data layer crashing OldDot). It was labelled
Externaland fixed by a contributor inMobile-ExpensifyPR #14049. This one can be worked the same way, so it does not necessarily need an internal engineer — just someone withMobile-Expensifywrite access.
Next Steps: Reply with
@MelvinBot <your feedback>to refine this analysis. I can't open a PR for this one, since the fix lands outsideExpensify/App.
Evidence (1)
Recordings
- [▶ Session recording of the verification run](https://melvinbot-assets.exops.io/session-33435196723-1-49dc4b7a-c340-4cfd-863a-56d063f9f5cf.webm)- Don't only grep for
@MelvinBot assign me my friend
Reacted by melvin-botDone — you're assigned,
GCyganek.Julesssssstays on as well.Heads up: the fix lands in
Mobile-Expensify(OldDot Swift), notExpensify/App— see my analysis above for the two guards that need changing.
@Julesssss, @GCyganek Eep! 4 days overdue now. Issues have feelings too...
- addedAwaiting PaymentAuto-added when associated PR is deployed to productionAuto-added when associated PR is deployed to production
on Sep 9, 2026 - changed the title
[-][Sentry: APP-4GM] HybridApp iOS OldDot SIGABRT — NSNull force cast to NSNumber in GuidedReview control banner[/-][+][Due for payment 2026-09-09] [Sentry: APP-4GM] HybridApp iOS OldDot SIGABRT — NSNull force cast to NSNumber in GuidedReview control banner[/+]on Sep 9, 2026 Triggered auto assignment to @mallenexpensify (
Awaiting Payment)Payment Summary
- Contributor: @GCyganek does not require payment (Contractor)
- Reviewer: @mkhutornyi owed $250 via NewDot
BugZero Checklist (@mallenexpensify)
- I have confirmed assignees, roles, and Upwork contracts look correct
- I have paid out Upwork contracts / manual NewDot requests
- [BugZero Assignee] I have created a GH issue for creating/updating the regression test once above steps have been agreed upon
Payment Summary
Contributor+: @mkhutornyi due $250 via NewDot
reckon we don't need a test case cuz it's Sentry
Fix deployed to prod a week ago (Sep 11), last event on Sep 10, I think we can wait another week to make sure this is fixed
- removed a parent issue
on Sep 23, 2026 approved $250 for @mkhutornyi
Only one event in the last 3 weeks and on an older version without the fix, looks like it works
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsDone
Sentry
https://expensify.sentry.io/issues/APP-4GM
Impact (snapshot at filing)
signalfatalGuidedReviewReportListViewControllerDelegate.setupControlBannerError
Stack trace (top frames, crashing thread)
Suspected cause
YAPL sends a control banner field as JSON
null. That bridges into Objective-C asNSNull, andsetupControlBannerforce casts it toNSNumber, which traps and aborts the process. The crashhappens during
viewDidLoadof the Guided Review report list, while the sheet presentation isbeginning, so the banner is built before the view is on screen and there is no chance to recover.
Two fixes, ideally both:
setupControlBanner, so a missing or null fielddegrades instead of aborting.
nullfor that field from the YAPL side, since a null there is presumably notmeaningful.
Reproduction
Open the Guided Review report list in OldDot on iOS for an account where the banner field comes back
null. Event context shows
session.uptime_bucket: 0-30sandnd.js_started: false, so this firesright after launch when Guided Review is the landing surface.
Related
Issue Owner
Current Issue Owner: @mallenexpensify