Skip to content

[Due for payment 2026-09-09] [Sentry: APP-4GM] HybridApp iOS OldDot SIGABRT — NSNull force cast to NSNumber in GuidedReview control banner #99853

Description

@mountiny

Sentry

https://expensify.sentry.io/issues/APP-4GM

Impact (snapshot at filing)

  • Users (total since first seen): 117
  • Events: 765
  • Users (last 14d): 17
  • First seen: 2025-12-28
  • Last seen: ongoing
  • Platform: iOS HybridApp, OldDot side (YAPL)
  • App version(s): 9.4.50-3, 9.4.53-10, 9.4.55-4, 9.4.57-3, 9.4.58-4, 9.4.61-4
  • Mechanism: signal
  • Level: fatal
  • Seer actionability: medium
  • Culprit: GuidedReviewReportListViewControllerDelegate.setupControlBanner

Error

SIGABRT: Could not cast value of type 'NSNull' to 'NSNumber'.

Stack trace (top frames, crashing thread)

abort
swift::fatalErrorv
swift::fatalError
swift::swift_dynamicCastFailure
swift_dynamicCast
GuidedReviewReportListViewControllerDelegate.setupControlBanner
ListViewController.setupControlBanner
ListViewController.invokeViewMethod
yaplControllerToViewControllerBridge.handleWithHeader
YAPLEnhancedCommandHandler.handle
-[YAPL yaplCall:]
... JavaScriptCore bridge frames ...
+[YAPLJS callFunction:args:]
-[BaseViewController postViewDidLoad]
ListViewController.postViewDidLoad
-[BaseViewController viewDidLoad]
-[UIViewController loadViewIfRequired]
-[UISheetPresentationController presentationTransitionWillBegin]

Suspected cause

YAPL sends a control banner field as JSON null. That bridges into Objective-C as NSNull, and
setupControlBanner force casts it to NSNumber, which traps and aborts the process. The crash
happens during viewDidLoad of the Guided Review report list, while the sheet presentation is
beginning, so the banner is built before the view is on screen and there is no chance to recover.

Two fixes, ideally both:

  1. Use a conditional cast with a sensible default in setupControlBanner, so a missing or null field
    degrades instead of aborting.
  2. Stop sending null for that field from the YAPL side, since a null there is presumably not
    meaningful.

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-30s and nd.js_started: false, so this fires
right after launch when Guided Review is the landing surface.

Related

Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. Julesssss commented on Aug 31, 2026

    @Julesssss
    Contributor

    @MelvinBot investigate this

  2. MelvinBot commented on Aug 31, 2026

    @MelvinBot
    Contributor

    Issue Analysis

    This crash cannot be fixed in Expensify/App. Every frame below the JavaScriptCore bridge is OldDot Swift that lives in Mobile-Expensify. App has no code path, native module method, config flag, or patch that reaches setupControlBanner — and Sentry's nd.js_started: false confirms it fires before NewDot's JS runtime even starts. Route this to someone with Mobile-Expensify access.

    Root cause: the description's hypothesis holds. YAPL runs in JavaScriptCore and calls setupControlBanner across the ObjC bridge. A JS null in that argument payload bridges to NSNull, and setupControlBanner force-casts it to a number. swift_dynamicCastFailure → SIGABRT. It happens inside viewDidLoad while UISheetPresentationController is 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:

    1. Don't only grep for as! NSNumber. Swift bridges as! Bool, as! Int, and as! Double from Any/AnyObject through NSNumber, so all of those produce this exact message. The offending line may not mention NSNumber at all.
    2. NSNull is not nil. if let x = dict["key"] and guard let both succeed on NSNull and then trap on the cast. The guard has to be as? NSNumber with a default, or an explicit is NSNull check.

    Fix both sides, as the description says: a conditional cast with a sensible default in setupControlBanner (stops the abort), plus removing the null from 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_banner across the whole App checkout matches only two unrelated help-site markdown articles under docs/articles/expensify-classic/. Zero code hits.
    • YAPL appears in App only as a comment in src/libs/Notification/PushNotification/index.native.ts:88 and as an opaque native startup-marker name in tests/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 after getHybridAppSettings() resolves and Onyx session data loads. That is strictly after the JS runtime starts, and this crash reports nd.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. The HybridApp mentions in patches/*/details.md are unrelated RN teardown crashes.

    What I could not confirm: the exact field name and source line. Mobile-Expensify is a private submodule that isn't checked out here, and I have no Sentry access in this run, so I can't read setupControlBanner or 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 External and fixed by a contributor in Mobile-Expensify PR #14049. This one can be worked the same way, so it does not necessarily need an internal engineer — just someone with Mobile-Expensify write 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 outside Expensify/App.


    view run

    Evidence (1)

    Recordings

    - [▶ Session recording of the verification run](https://melvinbot-assets.exops.io/session-33435196723-1-49dc4b7a-c340-4cfd-863a-56d063f9f5cf.webm)
  3. GCyganek commented on Sep 2, 2026

    @GCyganek
    Contributor

    @MelvinBot assign me my friend

  4. MelvinBot commented on Sep 2, 2026

    @MelvinBot
    Contributor

    Done — you're assigned, GCyganek. Julesssss stays on as well.

    Heads up: the fix lands in Mobile-Expensify (OldDot Swift), not Expensify/App — see my analysis above for the two guards that need changing.


    view run

  5. melvin-bot commented on Sep 7, 2026

    @melvin-bot

    @Julesssss, @GCyganek Eep! 4 days overdue now. Issues have feelings too...

  6. 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
  7. melvin-bot commented on Sep 9, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  8. melvin-bot commented on Sep 9, 2026

    @melvin-bot

    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
  9. mallenexpensify commented on Sep 10, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @mkhutornyi due $250 via NewDot

    reckon we don't need a test case cuz it's Sentry

  10. GCyganek commented on Sep 18, 2026

    @GCyganek
    Contributor

    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

  11. trjExpensify commented on Oct 1, 2026

    @trjExpensify
    Contributor

    approved $250 for @mkhutornyi

  12. GCyganek commented on Oct 2, 2026

    @GCyganek
    Contributor

    Only one event in the last 3 weeks and on an older version without the fix, looks like it works

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionDailyKSv2EngineeringImprovementItem broken or needs improvement.

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions