You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for payment 2026-08-03] Spends - Sort function on column briefly show an empty report page #96074
Version Number: 9.4.34-1 Reproducible in staging?: Yes Reproducible in production?: No If this was caught during regression testing, add the test name, ID and link from BrowserStack:#95758 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 / Chrome App Component: Search
Action Performed:
Preconditions: Fresh login to any account that has many report expenses
Steps:
Navigate to spends tab
Click on Report
Click the title column header to see the sort function
Expected Result:
The sort function works well without empty reports page briefly shown
Actual Result:
An empty reports page briefly shown when user clicking the title to sort it, even after user tried to clear cache and restart in troubleshoot. It also happens on other columns and on the expenses tab
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
Identify the pull request that introduced this issue and revert it.
Root Cause: The empty-page flash on sort is a regression from #95023 ("Allow workspace and status filter to be negatable", bernhardoj, merged 2026-07-13) — not from #95758, which Melvin auto-assigned. That PR tightened isSearchDataLoaded to require the loaded snapshot's hash to equal the current query hash. Since sortBy/sortOrder are folded into the query hash, clicking any column header produces a new hash and fires a fresh backend search under a new snapshot_<hash> key. While that snapshot loads, the page keeps showing the previous results as a fallback — but those carry the old hash, so isSearchDataLoaded now returns false, shouldShowEmptyState returns true even though data is present, and EmptySearchView flashes until the new snapshot arrives. This matches the symptom exactly: every sortable column, both Reports and Expenses tabs, staging-only.
Before #95023, that check compared type + status (neither changes on sort), so the old-hash fallback stayed "loaded" and no empty state appeared — which is why it does not reproduce in production.
Recommended Fix: Two options for the assignee to choose from:
Fix-forward: don't show the empty state while non-empty fallback results are being displayed during a sort/filter transition — e.g. have shouldShowEmptyState not fire when dataLength > 0, or treat the old-hash fallback as loaded while isSorting. This preserves the negatable-filter change while closing the flash.
Investigation details & evidence chain
Verified against App HEAD b2d9dadbf40d85694acdf4932f5f422549d2e5e4.
PR Pass unavailableTranslation param - batch 1 #95758 (the auto-assigned PR) only touched the sort comparators getSortedTransactionData/getSortedReportData (passing an unavailableTranslation param to getPolicyName). That path affects the ordering values, not the loading/empty-state gate, and only for the policy-name column — it cannot produce an empty flash across all columns and both tabs. Red herring.
Not a client-side sort perf issue — the sort itself is synchronous; the flash is caused by the fresh-snapshot fetch triggered by the hash change combined with the stricter loaded-check.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/96074) to create a draft PR (fix-forward option). To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's analysis.
The analysis above looks correct, but the root cause looks like an existing one. If we open prod and change the status filter, we can reproduce the same issue.
that check compared type + status (neither changes on sort)
The analysis above said that it doesn't happen before because we compare the type and status, and neither changes on sort. So, I tried changing the status filter and can observe the same issue on prod.
web.mp4
(try Clear cache & restart if you can't repro)
I think the expected result should be showing a skeleton loader.
[Contributor] The offending PR has been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.
Link to comment: Not regression. Pre-existing bug in production
[Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.
Link to discussion:
[Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.
[BugZero Assignee] Create a GH issue for creating/updating the regression test once above steps have been agreed upon.
⚠️ Looks like this issue was linked to a Deploy Blocker #96946
@aimane-chnaif please address ALL mentioned of deploy blockers, otherwise I have to go dig around to find out why it shouldn't be taken into consideration which takes time away from paying everyone. Thx
Activity
applause-bot commentedon Jul 14, 2026
While Applause doesn't embed attachments for this bug, here are the links for quick view:
https://platform.applause.com/services/links/v1/external/edd3a670bb9430cd311af84fd36fbf8bb24b1537b88dd2e841b9158e8a717cec
melvin-bot commentedon Jul 14, 2026
You have been assigned to this deploy blocker because you recently merged this PR: #95758
melvin-bot commentedon Jul 14, 2026
💬 A slack conversation has been started in #expensify-open-source
github-actions commentedon Jul 14, 2026
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
MelvinBot commentedon Jul 14, 2026
Issue Analysis
Root Cause: The empty-page flash on sort is a regression from #95023 ("Allow workspace and status filter to be negatable",
bernhardoj, merged 2026-07-13) — not from #95758, which Melvin auto-assigned. That PR tightenedisSearchDataLoadedto require the loaded snapshot's hash to equal the current query hash. Since sortBy/sortOrder are folded into the query hash, clicking any column header produces a new hash and fires a fresh backend search under a newsnapshot_<hash>key. While that snapshot loads, the page keeps showing the previous results as a fallback — but those carry the old hash, soisSearchDataLoadednow returnsfalse,shouldShowEmptyStatereturnstrueeven though data is present, andEmptySearchViewflashes until the new snapshot arrives. This matches the symptom exactly: every sortable column, both Reports and Expenses tabs, staging-only.Before #95023, that check compared
type+status(neither changes on sort), so the old-hash fallback stayed "loaded" and no empty state appeared — which is why it does not reproduce in production.Recommended Fix: Two options for the assignee to choose from:
shouldShowEmptyStatenot fire whendataLength > 0, or treat the old-hash fallback as loaded whileisSorting. This preserves the negatable-filter change while closing the flash.Investigation details & evidence chain
Verified against App HEAD
b2d9dadbf40d85694acdf4932f5f422549d2e5e4.The regressing diff (from
gh pr diff 95023):function isSearchDataLoaded(searchResults, queryJSON) { - const {status} = queryJSON ?? {}; - const sortedSearchResultStatus = ...status... - const sortedQueryJSONStatus = ...status... - const isDataLoaded = (data != null || errors != null) && search?.type === queryJSON?.type && sortedSearchResultStatus === sortedQueryJSONStatus; + const isDataLoaded = (data != null || errors != null) && search?.type === queryJSON?.type && searchResults.search.hash === queryJSON?.hash;End-to-end flow:
sortBy/sortOrder→navigation.setParams({q})(src/components/Search/index.tsx,onSortPress).primaryHash→ new hash → newSearch.search()→ empty newsnapshot_<hash>. (SearchQueryUtils.ts:600-607)SearchPage.tsxfeedslastNonEmptySearchResults(OLD hash) downward so the list still has rows.isSearchDataLoaded(searchResults, queryJSON)→ old hash ≠ newqueryJSON.hash→false. (SearchUIUtils.ts:4802-4806)shouldShowEmptyState(false, dataLength, type)→truedespitedataLength > 0→EmptySearchViewflashes. (SearchUIUtils.ts:4798-4800)Alternative explanations ruled out:
getSortedTransactionData/getSortedReportData(passing anunavailableTranslationparam togetPolicyName). That path affects the ordering values, not the loading/empty-state gate, and only for the policy-name column — it cannot produce an empty flash across all columns and both tabs. Red herring.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/96074)to create a draft PR (fix-forward option).To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's analysis.
bernhardoj commentedon Jul 14, 2026
The analysis above looks correct, but the root cause looks like an existing one. If we open prod and change the status filter, we can reproduce the same issue.
The analysis above said that it doesn't happen before because we compare the type and status, and neither changes on sort. So, I tried changing the status filter and can observe the same issue on prod.
web.mp4
(try Clear cache & restart if you can't repro)
I think the expected result should be showing a skeleton loader.
jasperhuangg commentedon Jul 14, 2026
Demoting since we can reproduce this in prod.
35 remaining items
melvin-bot commentedon Jul 30, 2026
@bernhardoj Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!
melvin-bot commentedon Aug 2, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)melvin-bot commentedon Aug 2, 2026
Payment Summary
Upwork Job
BugZero Checklist (@mallenexpensify)
aimane-chnaif commentedon Aug 3, 2026
BugZero Checklist:
[Contributor] The offending PR has been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.
Link to comment: Not regression. Pre-existing bug in production
[Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.
Link to discussion:
[Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.
[BugZero Assignee] Create a GH issue for creating/updating the regression test once above steps have been agreed upon.
Link to issue: https://github.com/Expensify/Expensify/issues/668914
Regression Test Proposal
Precondition:
Web only
Prerequisite: have some expenses
Test:
Do we agree 👍 or 👎
Zapier Logs
Run ID: 00040eee-6b91-a6c5-3f08-790c8348d2b4aimane-chnaif commentedon Aug 3, 2026
This ended up not being a regression from #95023. We fixed the root cause existed in production.
Reasoning: #96074 (comment)
mallenexpensify commentedon Aug 5, 2026
Payment Summary
Contributor: @bernhardoj due $250 via NewDot
Contributor+: @aimane-chnaif due $250 via NewDot
^ Test case created. Thx
@aimane-chnaif please address ALL mentioned of deploy blockers, otherwise I have to go dig around to find out why it shouldn't be taken into consideration which takes time away from paying everyone. Thx
flaviadefaria commentedon Aug 7, 2026
Approved: @bernhardoj due $250 via NewDot
garrettmknight commentedon Sep 14, 2026
$250 approved for @aimane-chnaif