Closed
Closed
Copy link
Labels
BugSomething is broken. Auto assigns a BugZero manager.Something is broken. Auto assigns a BugZero manager.DailyKSv2KSv2ExternalAdded to denote the issue can be worked on by a contributorAdded to denote the issue can be worked on by a contributorHelp WantedApply this label when an issue is open to proposals by contributorsApply this label when an issue is open to proposals by contributors
Description
If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!
Version Number: 9.3.67-3
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: #82786
Email or phone of affected tester (no customers): N/A
Issue reported by: Applause Internal Team
Bug source: Pull Request QA execution
Device used: Windows 11 Home/Chrome
App Component: Money Requests
Action Performed:
- Open https://staging.new.expensify.com/
- Troubleshoot and toggle Simulate failing network requests
- Click FAB
- Click on 'Track distance'
- Select another distance type than default one (ex: default is Map, select Manual)
- Create the distance expense
- Click on the FAB
- Select 'Track distance'
Expected Result:
The last-used distance type should not be updated.
Actual Result:
The last-used distance type is udpated
Workaround:
Unknown
Platforms:
- Android: App
- Android: mWeb Chrome
- iOS: App
- iOS: mWeb Safari
- iOS: mWeb Chrome
- Windows: Chrome
- MacOS: Chrome / Safari
Screenshots/Videos
Video.mp4
Upwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~022051866017210375016
- Upwork Job ID: 2051866017210375016
- Last Price Increase: 2026-05-13
Issue Owner
Current Issue Owner: @justinpersaudMetadata
Metadata
Assignees
Labels
BugSomething is broken. Auto assigns a BugZero manager.Something is broken. Auto assigns a BugZero manager.DailyKSv2KSv2ExternalAdded to denote the issue can be worked on by a contributorAdded to denote the issue can be worked on by a contributorHelp WantedApply this label when an issue is open to proposals by contributorsApply this label when an issue is open to proposals by contributors
Type
Projects
- StatusShow more project fieldsDone
Activity
MelvinBot commentedon May 6, 2026
Proposal
What is the root cause of that problem?
When a distance expense is created, the last-used distance type (
NVP_LAST_DISTANCE_EXPENSE_TYPE) is updated via OnyxoptimisticData— meaning it's applied immediately, before the API call completes. However, there is no correspondingfailureDataentry to roll it back when the API call fails. So when "Simulate failing network requests" is on (or any real network failure), the preference persists even though the expense wasn't actually created.The bug exists in two places:
TrackExpense.ts:2453-2458— optimistic update, no failure rollbackSplit.ts:2004-2010— same issueWhat changes do you think we should make in order to solve the problem?
Add
failureDataentries that revertNVP_LAST_DISTANCE_EXPENSE_TYPEto its previous value when the API call fails, in bothTrackExpense.tsandSplit.ts. The previous value needs to be passed into these functions (or read before the optimistic update) so it can be restored on failure.The correct pattern is already demonstrated right next to the bug — the
ODOMETER_DRAFThandling atTrackExpense.ts:2461-2472has bothoptimisticDataandfailureData. Apply the same pattern toNVP_LAST_DISTANCE_EXPENSE_TYPE.Next Steps for Contributor+ team: Reply with
@MelvinBot implement thisto create a draft PR,@MelvinBot <your feedback>to refine this analysis, or explain why you are rejecting Melvin's proposal.melvin-bot commentedon May 6, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @huult (
External)mukhrr commentedon May 6, 2026
🚨 Edited by proposal-police: This proposal was edited at 2026-05-11 17:00:43 UTC.
Verified: the pinned SHA
39d1d17e9b8…still resolves and the line numbers are correct (TrackExpense.ts L2442-L2448, Split.ts L1992-L1998), so the permalinks still render.Here's the corrected proposal to replace comment 4384844594 with:
Proposal
What is the root cause of that problem?
NVP_LAST_DISTANCE_EXPENSE_TYPEis written viaoptimisticData— the "remembered distance type" is recorded the instant the create is attempted, before it's known to succeed:App/src/libs/actions/IOU/TrackExpense.ts
Lines 2442 to 2448 in 39d1d17
App/src/libs/actions/IOU/Split.ts
Lines 1992 to 1998 in 39d1d17
So when the create fails (Simulate failing network requests, or any real failure), the attempted type is already persisted and drives the next Track-distance flow.
startDistanceRequestroutes off this NVP, andDistanceRequestStartPagefalls back to it / the persistedselectedTab_distanceRequestType:App/src/libs/actions/IOU/index.ts
Lines 493 to 505 in 39d1d17
App/src/pages/iou/request/DistanceRequestStartPage.tsx
Lines 65 to 71 in 39d1d17
Crucially, a "restore the previous value on
failureData" approach cannot fix this (this is why every prior proposal of that shape doesn't work). Empirically, with a restore-on-failure patch applied: attempt 1 reverts correctly (→ Map), but attempt 2 in the same session stays stuck on the failed type, and it only resets if you reload before the 2nd attempt. The reason: by the time the 2nd attempt captures the "previous" value, that value is itself the polluted optimistic value left by attempt 1's optimistic write (applied synchronously byAPI.write; the deferred-write path widens the window). Restoring to a corrupted baseline compounds the bug. A reload clears the in-memory pollution, which is why it then behaves correctly — proving the defect is the optimistic write itself, not a missing rollback.What changes do you think we should make in order to solve the problem?
Record the remembered type only once the create actually succeeds, and clear the immediately-persisted distance tab on failure (a plain clear — no captured "previous" value, so nothing can compound).
In
src/libs/actions/IOU/TrackExpense.ts(and the equivalent block increateDistanceRequestinSplit.ts):if (isDistanceRequest && isDistanceExpenseType(transaction?.iouRequestType)) { - onyxData?.optimisticData?.push({ + onyxData?.successData?.push({ onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, value: transaction?.iouRequestType, }); + onyxData?.failureData?.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.DISTANCE_REQUEST_TYPE}`, + value: null, + }); }successDataensures a pending/failed/offline attempt never records the type, so there is no optimistic value to leak and nothing to "restore" — attempt N behaves identically to attempt 1, with or without a reload. The unconditionalfailureDataclear ofselectedTab_distanceRequestTypehandles the tab thatOnyxTabNavigatorpersists the moment the user switches it (independent of the request), whichDistanceRequestStartPagefalls back to. Supporting change: addtypeof ONYXKEYS.COLLECTION.SELECTED_TABto theOnyxData<…>key unions inTrackedExpenseParams.tsand the local unions inshareTrackedExpense/createDistanceRequestso theselectedTab_entry typechecks.What alternative solutions did you explore? (Optional)
N/A
melvin-bot commentedon May 6, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022051866017210375016
[-]Latest distance type is updated in 'Simulate failing network requests' mode[/-][+][$250] Latest distance type is updated in 'Simulate failing network requests' mode[/+]aswin-s commentedon May 6, 2026
🚫 Duplicated proposal withdrawn by 🤖 ProposalPolice.
github-actions commentedon May 6, 2026
wildan-m commentedon May 6, 2026
🚫 Duplicated proposal withdrawn by 🤖 ProposalPolice.
trasnake87 commentedon May 6, 2026
Proposal
The "last-used distance type" preference is being persisted optimistically with no rollback, so toggling "Simulate failing network requests" (or any real network failure) leaves the NVP updated even though the expense never made it to the server.
Root cause
NVP_LAST_DISTANCE_EXPENSE_TYPEis written intooptimisticDatain two places, neither of which adds a correspondingfailureDataentry to revert the value when the API call fails. Onyx applies the optimistic write immediately, the request errors out under the simulator, and because there is no failure entry, the new value sticks — driving the next "Track distance" tap to the wrong default.1 -
trackExpenseflow (src/libs/actions/IOU/TrackExpense.ts:2448-2457):Compare this with the
NVP_RECENT_WAYPOINTShandling 9 lines above, which does push a correspondingfailureDataentry that restoresrecentServerValidatedWaypoints(src/libs/actions/IOU/TrackExpense.ts:2439-2444). The distance-type write is missing that symmetric rollback.2 -
createDistanceRequestflow (src/libs/actions/IOU/Split.ts:1996-2002):Same shape, same omission. This is the path used when the user is on the global FAB → Track distance flow and a single (non-track) distance request is created — which matches the reproduction steps in the issue (FAB → Track distance → Manual → submit).
3 - Why this surfaces only under the failure simulator:
In normal use the API succeeds, the optimistic value happens to coincide with what the server would have stored anyway, so the bug is invisible. With "Simulate failing network requests" on, every write fails and the missing
failureDatabecomes observable — the preference is now whatever the user attempted, not what was actually saved.Proposed fix
Capture the prior value of
NVP_LAST_DISTANCE_EXPENSE_TYPEvia a module-levelOnyx.connect(the same pattern used forallPersonalDetails/allTransactionsinsrc/libs/actions/IOU/index.ts:285-358), then add afailureDataentry that restores it when the optimistic distance-type write is queued.Change 1 —
src/libs/actions/IOU/TrackExpense.tsAdd a module-level cache near the existing top-of-file imports/state (i.e. immediately after the imports block, before
function trackExpense(...)):Then replace
src/libs/actions/IOU/TrackExpense.ts:2451-2457with:Make sure
DistanceExpenseTypeis imported (it's already exported fromsrc/types/onyx/IOU.ts:234and re-exported viaOnyxTypes).Change 2 —
src/libs/actions/IOU/Split.tsAdd the same module-level connect once near the top of the file:
Then replace
src/libs/actions/IOU/Split.ts:1996-2002with:Using
?? null(rather than?? undefined) is important — the very first time a user creates a distance expense, the NVP doesn't exist yet, and on failure the value should go back to "unset" so consumers likeuseExpenseActions.ts:148andDistanceRequestStartPage.tsx:49fall back to their defaults exactly as they did before the failed attempt.Onyx.METHOD.SETwithnullclears the key.Why this approach
NVP_RECENT_WAYPOINTS10 lines above the bug (TrackExpense.ts:2440-2444,Split.ts:2047-2051) — the same offline-first rollback pattern, applied to the same kind of NVP. Reviewers don't need to reason about a new mechanism.Onyx.connectis the established way both action files already read live NVPs/collections without forcing every caller to thread a new prop through (see theallPersonalDetails/allTransactionDraftsconnects insrc/libs/actions/IOU/index.ts:285-358). The alternative — adding alastDistanceExpenseTypeparameter totrackExpenseandcreateDistanceRequest— was rejected because it would touch every caller (MoneyRequest.ts:215,MoneyRequest.ts:670, plusShare/SubmitDetailsPage.tsx,IOURequestStepDistanceManual.tsx,IOURequestStepDistanceMap.tsx,IOURequestStepDistanceGPS/index.native.tsx,IOURequestStepAmount.tsx) for no behavioral gain.undefinedfirst-time case — instead of guessing it from the new transaction's type.grep -rn "NVP_LAST_DISTANCE_EXPENSE_TYPE" src/libs/actions/confirms no other writers exist), so no other call paths can regress.33 remaining items
MelvinBot commentedon May 18, 2026
The bug was introduced by PR #69372 ("[Manual distance] Create the distance creation tabs v2", merged 2025-08-29).
That PR added the
NVP_LAST_DISTANCE_EXPENSE_TYPEOnyx key and wrote it viaoptimisticDataincreateDistanceRequestwithout a correspondingfailureDataentry to roll it back on API failure. The same pattern was later carried intotrackExpensewhen the code was refactored (the extraction intoTrackExpense.ts/Split.ts).The feature was initially behind a
MANUAL_DISTANCEbeta flag. PR #71474 ("Remove the manual distance beta", merged 2025-09-29) removed that gate, exposing the bug to all users — though it was only caught during QA of v9.3.67.huult commentedon May 18, 2026
Thanks for all proposal.
@emkhalid @dilshodmackbook-sketch Your solution can cause a bug if you only update successData, because it waits for the API response before updating the value. So before the response returns, the value is not updated, meaning it won’t update immediately.
Screen.Recording.2026-05-18.at.10.18.12.mov
@mukhrr Did you test your proposal? If you're sure it works, could you share your test branch or record a video and post it here?
huult commentedon May 18, 2026
Looking for the proposal
emkhalid commentedon May 18, 2026
@huult thanks again for the feedback. I updated my proposal with a different approach.
Instead of writing the confirmed last distance type optimistically, the updated proposal keeps
NVP_LAST_DISTANCE_EXPENSE_TYPEas the last successfully created distance type, and uses a temporary pending distance type only for the immediate pre-response UX. I also stopped treatingselectedTab_distanceRequestTypeas persisted state for the distance flow, so it should not keep a failed tab selection in IndexedDB.This should preserve the immediate update you pointed out while keeping the failed-request state clean. I’ll test this version and attach a fresh video demo.
mukhrr commentedon May 18, 2026
@huult here is demo of the fix and also updated proposal
REC-20260518092355.mp4
huult commentedon May 20, 2026
Screen.Recording.2026-05-20.at.11.37.12.mov
App/src/libs/Navigation/OnyxTabNavigator.tsx
Lines 197 to 199 in b984e6b
After reviewing and tracing the code changes again, I don’t think this is a bug. It looks like expected behavior. I’d like to ask Internal to confirm before making a decision.
Hi Internal team, could you help review the expected behavior for this ticket?
When tracking distance and the network is offline or fails:
I successfully track distance online with selected tab = manual
Then I switch the tab to GPS and submit while offline or with a failed network
When I open the tracking distance tab again, should the selected type be GPS or manual?
The expected behavior in this ticket is
manual, but I think it should be GPS since we don’t need to restore the previously selected online tab. Could you please confirm if this is correct?🎀 👀 🎀 c+ reviewed.
melvin-bot commentedon May 20, 2026
Triggered auto assignment to @justinpersaud, see https://stackoverflow.com/c/expensify/questions/7972 for more details.
nicktofel208 commentedon May 20, 2026
🚫 Duplicated proposal withdrawn by 🤖 ProposalPolice.
melvin-bot commentedon May 20, 2026
📣 @nicktofel208! 📣
Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
Please follow these steps:
Format:
github-actions commentedon May 20, 2026
melvin-bot commentedon May 20, 2026
@justinpersaud @huult this issue was created 2 weeks ago. Are we close to approving a proposal? If not, what's blocking us from getting this issue assigned? Don't hesitate to create a thread in #expensify-open-source to align faster in real time. Thanks!
justinpersaud commentedon May 21, 2026
What @huult posted is correct. Lets just close this.