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-09-25] Expense-Empty report can be downloaded via reconciliation in workspace chat #100701
Version Number: V9.4.74-0 Reproducible in staging?: Yes Reproducible in production?: N/A - new feature, doesn't exist in prod If this was caught during regression testing, add the test name, ID and link from BrowserStack:#99309 Email or phone of affected tester (no customers):dreamnitethedragon+vegst@gmail.com Issue reported by: Applause Internal Team Bug source: Exploratory - Significant User Experience Deterioration Device used: Redmi note 10s android 13 App Component: User Settings
Action Performed:
Launch app
Open workspace chat
Create empty report
Tap more - expense - reconciliation option - note download completes
Go to Spend - Report
Long press and select report - export - reconciliation option
Note download fail message is shown
Expected Result:
Empty report must not be able to be downloaded via reconciliation in workspace chat like in spend - report.
Actual Result:
Empty report can be downloaded via reconciliation in workspace chat but show failed in spend - report.
👋 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.
Classification: Frontend bug Causing PR: #99309 - "Add "Reconciliation - All Expenses" export option for NewDot" by @dukenv0307 (High confidence for the new symptom, see caveat below) Related Issues: #99172 (the issue #99309 implemented)
Recommendation: ROLL FORWARD
The new Reconciliation - All Expenses template leaked out of the Search export menu (its intended scope) into the report header's More > Export submenu, which — unlike Search — has no empty-report guard. The fix is a ~6-line guard in a single hook, so a targeted fix is much cheaper and lower-risk than reverting an approved feature PR.
Assigned: @dukenv0307 (PR author), @NikkiWines and @ZhenjaHorbach (approving reviewers) — already assigned by Melvin, no change needed Labels: No changes. DeployBlockerCash is correct and is kept (App-side fix). DeployBlocker was not present, so nothing was removed.
📋 Detailed Analysis
Evidence
1. #99309 is the only PR on the staging checklist (#100624) that touches reconciliation export. Grepping the checklist body for reconcil/export returns only #99309 (checklist line 45). Merged 2026-09-08T14:50:58Z, immediately before this v9.4.74 report — timeline matches, and the reporter confirms the option does not exist in production.
2. #99309 added the template behind a defaulted parameter of a shared function, so every caller that omits the argument silently opts in:
getExportTemplates has five callers. Only the Search bulk-actions hook was updated to pass the new flag explicitly (useSearchBulkActions.ts#L1832-L1842). The report-level callers were not, so they inherited the template:
useExportActions.ts#L74-L83 — the report header More > Export submenu (the workspace-chat flow in the repro). Passes 8 positional args and stops at includeMultipleTaxExport, so the 9th falls through to the default.
The PR description scopes the change to "the Search export menu … including from Card statements". The report header menu was neither intended nor tested — the PR's tests cover only getExportTemplates and useSearchBulkActions (tests/unit/SearchActionsTest.ts, tests/unit/hooks/useSearchBulkActionsExportTest.ts), with no coverage of useExportActions.
3. Production vs staging: staging-only for the reported symptom, as the reporter states. Caveat below on the underlying guard gap.
Verification
Confirmed the "download failed" behaviour the reporter saw in Spend > Report is a client-side guard, and that it exists in exactly one of the two flows:
emptyReports is derived from fullReport.transactionCount === 0. That state renders the modal at SearchBulkActionsButton.tsx#L295-L303 with the copy common.downloadFailedEmptyReportDescription → "You can't export an empty report." (en.ts#L212-L215). This is exactly the message in step 7 of the repro.
Guard absent (workspace chat report header) — useExportActions.ts#L125-L150. Its beginExportWithTemplate only checks isOffline and !moneyRequestReport, then queues the export with transactionIDList: transactionIDs — an empty array for an empty report:
Both flows then hit the same QueueExportSearchWithTemplate command (Search.ts#L1925-L1975), so the only thing producing the divergence in behaviour is the missing client guard.
Also confirmed the export entry point itself is ungated: SECONDARY_ACTIONS.EXPORT is pushed unconditionally at ReportSecondaryActionUtils.ts#L1137, with no transaction-count check.
Root Cause
Two layers:
Regression from Add "Reconciliation - All Expenses" export option for NewDot #99309 (the reported symptom). Adding includeReconciliationAllExpenses as a parameter whose default is isAdminOfCardEnabledPolicy(policy) — rather than false with explicit opt-in — turned the new template on for every existing getExportTemplates caller. The report header and transaction-selection export menus picked it up for free, outside the PR's intended and tested scope.
Pre-existing gap it exposed. The empty-report guard was only ever implemented in useSearchBulkActions, never in useExportActions. So on an empty report the header menu will happily queue any template export (Expense level export, Report level export, custom templates), not just reconciliation. That part is not new in Add "Reconciliation - All Expenses" export option for NewDot #99309 — it just became visible because reconciliation is the template most obviously wrong to run against an empty report. Worth a separate follow-up issue.
There is a second, product-level question for @trjExpensify / @NikkiWines: Reconciliation - All Expenses reports on workspace-wide card spend, which is why Search computes eligibility across all policies rather than from the selection. Scoping it to a single report via reportIDList: [reportID] is semantically odd regardless of whether the report is empty, so it may not belong in the single-report export menu at all.
Suggested fix
Primary (unblocks the deploy), in src/hooks/useExportActions.ts — add the empty-report guard to beginExportWithTemplate, mirroring Search:
if((moneyRequestReport.transactionCount??transactionIDs.length)===0){// Match the Search export flow: an empty report has nothing to exportshowDownloadErrorModal();return;}
transactionCount is on the Report type (Report.ts#L171). To match the Search copy exactly, showDownloadErrorModal should take an optional prompt so it can use common.downloadFailedEmptyReportDescription instead of the generic common.downloadFailedDescription.
Revert is not recommended: #99309 spans 22 files including 11 language files and three test files, the feature itself works as designed in Search and Card statements, and the defect is one missing guard in an adjacent hook.
💡 Suggested AI code review rule
This bug looks like a recurring, mechanically detectable pattern, so it may be worth a coding-standards rule so the AI reviewer catches the next one. Proposed for Expensify/App:
Rule:CONSISTENCY-18 Opt new behavior in explicitly at every call site
Condition: When a PR adds a new parameter to a shared, multi-caller function, flag it if the default is anything other than the behavior-preserving value (e.g. = someHelper(x) or = a === b rather than = false). Default it off and pass it explicitly at the call sites that want it, so untouched callers keep their current behavior.
Why: A defaulted-on parameter silently enables the new behavior in every caller the PR never opened or tested. Here it pushed a workspace-wide card reconciliation export template into the single-report export menus, which lack the empty-report guard the Search menu has — #100701.
Search pattern: in the diff, ^\+\s+\w+\s*=\s*(?!(true|false|undefined|null|\[\]|\{\}|['"0-9]))inside an exported function's parameter list; thengrep -rn "(" src/` and check whether every call site was updated in the same PR.
The full rule file, with the ❌/✅ code inline in Expensify/App's coding-standards format, would go in Expensify/App itself. If it's worth adding, a maintainer can open it there as a draft PR.
[Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.
Link to the comment on the PR: NA
Link to the comment on the Issue: NA
It's more like a new feature
[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: NA
[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.
Precondition:
Test:
Open App
Open workspace chat
Create an empty report
Tap More - Export
Click any kind of export
Verify that: An empty report must not be able to be downloaded, and EmptyReportDownloadErrorModal should be shown
Activity
applause-bot commentedon Sep 9, 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/9bb1cc460457794b09b9ee525b3c842392bcae81527ef4225e63bed8266aa742
melvin-bot commentedon Sep 9, 2026
You have been assigned to this deploy blocker because you recently merged this PR: #99309
github-actions commentedon Sep 9, 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:
melvin-bot commentedon Sep 9, 2026
@NikkiWines @ZhenjaHorbach you have been assigned to this deploy blocker because you reviewed the offending PR: #99309
melvin-bot commentedon Sep 9, 2026
@dukenv0307 you have been assigned to this deploy blocker because you authored the offending PR: #99309
melvin-bot commentedon Sep 9, 2026
💬 A slack conversation has been started in #expensify-open-source
MelvinBot commentedon Sep 9, 2026
🔍 Investigation Summary
Classification: Frontend bug
Causing PR: #99309 - "Add "Reconciliation - All Expenses" export option for NewDot" by @dukenv0307 (High confidence for the new symptom, see caveat below)
Related Issues: #99172 (the issue #99309 implemented)
Recommendation: ROLL FORWARD
The new
Reconciliation - All Expensestemplate leaked out of the Search export menu (its intended scope) into the report header's More > Export submenu, which — unlike Search — has no empty-report guard. The fix is a ~6-line guard in a single hook, so a targeted fix is much cheaper and lower-risk than reverting an approved feature PR.Assigned: @dukenv0307 (PR author), @NikkiWines and @ZhenjaHorbach (approving reviewers) — already assigned by Melvin, no change needed
Labels: No changes.
DeployBlockerCashis correct and is kept (App-side fix).DeployBlockerwas not present, so nothing was removed.📋 Detailed Analysis
Evidence
1. #99309 is the only PR on the staging checklist (#100624) that touches reconciliation export. Grepping the checklist body for
reconcil/exportreturns only #99309 (checklist line 45). Merged2026-09-08T14:50:58Z, immediately before this v9.4.74 report — timeline matches, and the reporter confirms the option does not exist in production.2. #99309 added the template behind a defaulted parameter of a shared function, so every caller that omits the argument silently opts in:
src/libs/actions/Search.ts#L2007-L2016—getExportTemplates(..., includeReconciliationAllExpenses = isAdminOfCardEnabledPolicy(policy))src/libs/actions/Search.ts#L2048-L2053— where the template is pushedsrc/libs/PolicyUtils.ts#L820-L825— the newisAdminOfCardEnabledPolicyhelpergetExportTemplateshas five callers. Only the Search bulk-actions hook was updated to pass the new flag explicitly (useSearchBulkActions.ts#L1832-L1842). The report-level callers were not, so they inherited the template:useExportActions.ts#L74-L83— the report header More > Export submenu (the workspace-chat flow in the repro). Passes 8 positional args and stops atincludeMultipleTaxExport, so the 9th falls through to the default.useSelectedTransactionsActions.ts#L396-L404— transaction long-press export inside a report. Same leak.The PR description scopes the change to "the Search export menu … including from
Card statements". The report header menu was neither intended nor tested — the PR's tests cover onlygetExportTemplatesanduseSearchBulkActions(tests/unit/SearchActionsTest.ts,tests/unit/hooks/useSearchBulkActionsExportTest.ts), with no coverage ofuseExportActions.3. Production vs staging: staging-only for the reported symptom, as the reporter states. Caveat below on the underlying guard gap.
Verification
Confirmed the "download failed" behaviour the reporter saw in Spend > Report is a client-side guard, and that it exists in exactly one of the two flows:
Guard present (Search / Spend > Report) —
useSearchBulkActions.ts#L846-L862:emptyReportsis derived fromfullReport.transactionCount === 0. That state renders the modal atSearchBulkActionsButton.tsx#L295-L303with the copycommon.downloadFailedEmptyReportDescription→ "You can't export an empty report." (en.ts#L212-L215). This is exactly the message in step 7 of the repro.Guard absent (workspace chat report header) —
useExportActions.ts#L125-L150. ItsbeginExportWithTemplateonly checksisOfflineand!moneyRequestReport, then queues the export withtransactionIDList: transactionIDs— an empty array for an empty report:Both flows then hit the same
QueueExportSearchWithTemplatecommand (Search.ts#L1925-L1975), so the only thing producing the divergence in behaviour is the missing client guard.Also confirmed the export entry point itself is ungated:
SECONDARY_ACTIONS.EXPORTis pushed unconditionally atReportSecondaryActionUtils.ts#L1137, with no transaction-count check.Root Cause
Two layers:
Regression from Add "Reconciliation - All Expenses" export option for NewDot #99309 (the reported symptom). Adding
includeReconciliationAllExpensesas a parameter whose default isisAdminOfCardEnabledPolicy(policy)— rather thanfalsewith explicit opt-in — turned the new template on for every existinggetExportTemplatescaller. The report header and transaction-selection export menus picked it up for free, outside the PR's intended and tested scope.Pre-existing gap it exposed. The empty-report guard was only ever implemented in
useSearchBulkActions, never inuseExportActions. So on an empty report the header menu will happily queue any template export (Expense level export, Report level export, custom templates), not just reconciliation. That part is not new in Add "Reconciliation - All Expenses" export option for NewDot #99309 — it just became visible because reconciliation is the template most obviously wrong to run against an empty report. Worth a separate follow-up issue.There is a second, product-level question for @trjExpensify / @NikkiWines:
Reconciliation - All Expensesreports on workspace-wide card spend, which is why Search computes eligibility across all policies rather than from the selection. Scoping it to a single report viareportIDList: [reportID]is semantically odd regardless of whether the report is empty, so it may not belong in the single-report export menu at all.Suggested fix
Primary (unblocks the deploy), in
src/hooks/useExportActions.ts— add the empty-report guard tobeginExportWithTemplate, mirroring Search:transactionCountis on theReporttype (Report.ts#L171). To match the Search copy exactly,showDownloadErrorModalshould take an optional prompt so it can usecommon.downloadFailedEmptyReportDescriptioninstead of the genericcommon.downloadFailedDescription.Optional, if product decides the workspace-wide template shouldn't appear in single-report menus: pass
includeReconciliationAllExpenses: falseexplicitly fromuseExportActions.ts#L74-L83anduseSelectedTransactionsActions.ts#L396-L404.Revert is not recommended: #99309 spans 22 files including 11 language files and three test files, the feature itself works as designed in Search and Card statements, and the defect is one missing guard in an adjacent hook.
💡 Suggested AI code review rule
This bug looks like a recurring, mechanically detectable pattern, so it may be worth a coding-standards rule so the AI reviewer catches the next one. Proposed for
Expensify/App:CONSISTENCY-18Opt new behavior in explicitly at every call site= someHelper(x)or= a === brather than= false). Default it off and pass it explicitly at the call sites that want it, so untouched callers keep their current behavior.^\+\s+\w+\s*=\s*(?!(true|false|undefined|null|\[\]|\{\}|['"0-9]))inside an exported function's parameter list; thengrep -rn "(" src/` and check whether every call site was updated in the same PR.src/libs/actions/Search.ts#L2007-L2016and the four call sites that were left untouched (one, two)falseand pass explicitly, asuseSearchBulkActions.ts#L1832-L1842already doesThe full rule file, with the ❌/✅ code inline in
Expensify/App's coding-standards format, would go inExpensify/Appitself. If it's worth adding, a maintainer can open it there as a draft PR.15 remaining items
NikkiWines commentedon Sep 23, 2026
i think it's a separate issue since this behavior existed for other templates as well
ZhenjaHorbach commentedon Sep 23, 2026
Contributor+ Checklist:
[Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.
Link to the comment on the PR: NA
Link to the comment on the Issue: NA
It's more like a new feature
[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: NA
[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.
Precondition:
Test:
Do we agree 👍 or 👎
melvin-bot commentedon Sep 24, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)melvin-bot commentedon Sep 24, 2026
Payment Summary
Resolving PRs:
Expense-Empty report can be downloaded via reconciliation in workspace chat #101048
Reviewer: @ZhenjaHorbach owed $175 via NewDot
Contributor: @dukenv0307 does not require payment (Contractor)
BugZero Checklist (@mallenexpensify)
mallenexpensify commentedon Sep 26, 2026
Payment Summary
Contributor+: @ZhenjaHorbach due $250 via NewDot
@ZhenjaHorbach can you use the exact C+ checklist? I have automation to key off
## Regression Test Proposalin the comment. Thx.garrettmknight commentedon Nov 5, 2026
$250 approved for @ZhenjaHorbach