whisper-money/resources/js/lib
Víctor Falcón 5ec62d8625
fix(observability): stop our own page navigations from reporting fake network errors (#780)
## The issue

`AxiosError: Network Error` (PHP-LARAVEL-28) is the noisiest issue in
this project — 192 events / 75 users, `handled: no` — and it has been
archived as ambient connectivity noise several times, including by me.
The event distribution says otherwise:

| URL | events (last 24) |
|---|---|
| `/onboarding` | 15 |
| `/register` | 3 |
| `/dashboard`, `/accounts`, `/accounts/{id}`, `/settings/connections`,
`/` | 1 each |

Every one of those is a page that triggers a full-page navigation out of
the SPA, and the events skew heavily to Safari/macOS.

## The mechanism

Assigning `window.location` aborts every request still in flight.
Browsers report that abort to XHR through `onerror`, as a transport
failure rather than a cancellation — which is why it arrives as `Network
Error` and not `Request aborted`. Inertia rethrows it, so it lands as an
unhandled rejection.

The onboarding bank-connection step is the perfect generator: it polls
every 4s (`usePoll` in `pages/onboarding/index.tsx:124`) and *then*
sends the user to their bank with `window.location.href`
(`hooks/use-connect-flow.ts:219`). A poll dying mid-flight is close to
guaranteed.

So this is a bug report for a request that never failed. The point isn't
the volume — it's that until now a real "the user's connection dropped
and their action silently did nothing" was indistinguishable from our
own navigations.

## The change

`leavePage()` / `reloadPage()` record the departure; a `beforeSend`
predicate drops transport-level failures while that flag is set. It
follows the five sibling noise predicates already in `lib/sentry.ts`, so
Inertia's internals stay untouched. 13 call sites converted.

Commits are one-per-finding from the two reviews, and the two
interesting ones are corrections to my own first attempt:

- **`b7dc8dc6` — the first version was inert.** `HttpError`'s
constructor appends the request URL (`super(url ? \`${message}
(${url})\` : message)`), so Inertia's XHR client — the default in
v3.6.1, which we moved to in #769 — rejects with `Network error
(https://whisper.money/onboarding?step=create-account)`, not `Network
error`. My anchored regex matched neither. The 192 sampled events read
`AxiosError: Network Error` only because they were produced by **v2**,
which went through axios. Verified with a runtime probe against the
installed package, and the test row now carries the real string (it
fails against the previous pattern).
- **`09ea3b01` — the flag outlived the navigation.** I had it one-way on
the assumption it dies with the document. Two flows here keep the
document alive, both in the Safari/iOS population this issue skews to:
an **iOS PWA** hands the bank redirect to Safari and stays alive polling
(`pages/onboarding/index.tsx:121` documents this — it's why the poll
exists), and **bfcache** restores the heap when the user presses Back
from the bank or Stripe. Either way the user carries on in a live page
with reporting silenced for the rest of the session. Now cleared by a
persisted `pageshow` or by becoming visible again; neither can clear a
departure genuinely in progress, since a same-window redirect never
hides the page.

## Why `beforeSend` and not `router.on('networkError')`

Inertia does expose a cancellable event, and `preventDefault()` would
stop the rejection at the source. Don't — it also skips
`onPrefetchError`, which is where the cleanup lives:

```js
onPrefetchError(error) { prefetchedRequests.removeFromInFlight(params); reject(error) }
```

A stale `inFlightRequests` entry isn't cosmetic: `add()` early-returns
whenever `findInFlight` hits, and `get()` returns it with a promise that
never settles — so that URL becomes un-prefetchable for the session and
a `Link` consuming it hangs. We have `prefetch` on the sidebar and user
menu. It would also suppress genuine errors unconditionally, which is
the opposite of the goal.

## Deliberately not done

**Feedback on a real network failure.** When an Inertia visit genuinely
fails, the user still gets nothing — they click and nothing happens. A
toast needs to exclude background prefetch and poll requests; v3 does
make that possible (`visit.prefetch` / `visit.poll` on
`router.on('start')`), so this is a scope call, not an impossibility.
Worth its own PR.

**A bad bank `redirect_url`.** Fails at the browser level, so it was
never a JS event — we lose nothing here, but we also have no in-app
feedback and the only trace is a connection stranded in `Pending`.
Separate ticket.

## Verification

369/369 JS tests, lint, format, `dry` (4.81% vs 5.40% threshold),
`build` and `build:ssr` all green. Each behavioural test was confirmed
to **fail** with its fix reverted — including the stale-count one, which
reproduces the wrong number on screen.

SSR needed care: `config/inertia.php` has it enabled and `ssr.tsx` globs
pages that import this module, so the listeners sit behind `typeof
window` and I verified the module imports cleanly in a node environment.

## Why this is a draft

Two things I'd rather you weighed:

1. **Suppressing errors fails silently.** If the gate is ever wrong we
go blind to a class of real failures, and no error tells us we stopped
getting errors — the same shape as the observability gap noted on #723.
The risk is bounded by the resets and the narrow message pattern, but
the direction of failure is under-reporting.
2. **My confidence here was already miscalibrated once.** The first
version passed every local check and one reviewer's independent
verification, and was still completely inert for the target traffic.
That argues for a human look rather than auto-merge.

The parts I'd merge without hesitation are `6a7059d4` (the onboarding
wrong-count fix, independent of all this) and `b7dc8dc6`. If you want
the observability change split from the onboarding one, say so and I'll
separate them.

Prod verification after deploy: PHP-LARAVEL-28 should stop accruing new
events on `/onboarding`, while `Network Error` events from users who
*stayed* on a page should keep arriving. If both go quiet, the gate is
too wide.
2026-08-11 16:12:00 +00:00
..
banking-connections.test.ts fix(open-banking): only block re-adding a bank when a live connection exists (#569) 2026-06-20 13:09:04 +02:00
banking-connections.ts fix(open-banking): only block re-adding a bank when a live connection exists (#569) 2026-06-20 13:09:04 +02:00
category-tree.test.ts feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
category-tree.ts feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
chart-calculations.test.ts fix(dashboard): render negative net worth as a downward bar (#722) 2026-07-22 09:32:18 +00:00
chart-calculations.ts fix(dashboard): render negative net worth as a downward bar (#722) 2026-07-22 09:32:18 +00:00
chart-tooltip-position.test.ts fix(chart): stop tooltip render loop crashing the dashboard (PHP-LARAVEL-3B) (#657) 2026-07-07 18:39:09 +00:00
chart-tooltip-position.ts fix(chart): stop tooltip render loop crashing the dashboard (PHP-LARAVEL-3B) (#657) 2026-07-07 18:39:09 +00:00
chunk-load-recovery.test.ts Fix Vite asset preload recovery (#399) 2026-05-14 15:59:08 +02:00
chunk-load-recovery.ts fix(observability): stop our own page navigations from reporting fake network errors (#780) 2026-08-11 16:12:00 +00:00
connect-providers.tsx feat(banking): enable Interactive Brokers for all users (#593) 2026-06-26 11:03:21 +02:00
crypto.ts refactor(encryption): strip client-side transaction encryption (#514) 2026-06-20 16:13:26 +00:00
csrf.test.ts refactor(js): extract shared getCsrfToken util (#475) 2026-06-03 17:26:09 +02:00
csrf.ts refactor(js): extract shared getCsrfToken util (#475) 2026-06-03 17:26:09 +02:00
cursor-pagination.test.ts Fix Sentry transaction and dashboard crashes (#372) 2026-05-10 11:10:31 +01:00
cursor-pagination.ts Fix Sentry transaction and dashboard crashes (#372) 2026-05-10 11:10:31 +01:00
debug.ts fix: make useIsMobile hook and utility functions SSR-safe 2025-12-08 17:33:54 +01:00
dexie-db.ts fix(sync): don't crash when IndexedDB is unavailable (PHP-LARAVEL-43) (#654) 2026-07-07 04:59:10 +00:00
file-parser.test.ts fix(import): honor selected date format for CSV imports (#494) 2026-06-05 15:10:48 +02:00
file-parser.ts fix(import): honor selected date format for CSV imports (#494) 2026-06-05 15:10:48 +02:00
import-config-storage.ts refactor(frontend): call backend routes through Wayfinder instead of literal URLs (#772) 2026-08-11 13:26:10 +00:00
key-storage.ts refactor(encryption): strip client-side transaction encryption (#514) 2026-06-20 16:13:26 +00:00
leave-page.test.ts fix(observability): stop our own page navigations from reporting fake network errors (#780) 2026-08-11 16:12:00 +00:00
leave-page.ts fix(observability): stop our own page navigations from reporting fake network errors (#780) 2026-08-11 16:12:00 +00:00
media-query.test.ts fix(appearance): support MediaQueryList change events on legacy Safari (PHP-LARAVEL-41) (#646) 2026-07-05 17:03:35 +00:00
media-query.ts fix(appearance): support MediaQueryList change events on legacy Safari (PHP-LARAVEL-41) (#646) 2026-07-05 17:03:35 +00:00
new-transactions.test.ts feat(transactions): make new-transaction marker cross-device (#611) 2026-06-29 19:11:37 +02:00
new-transactions.ts feat(transactions): make new-transaction marker cross-device (#611) 2026-06-29 19:11:37 +02:00
orphan-components.test.ts chore(frontend): add orphan component detection and remove dead components (#181) 2026-03-02 12:43:27 +01:00
posthog.test.ts Harden browser storage and PostHog recording (#402) 2026-05-14 15:57:48 +02:00
posthog.ts feat(posthog): route analytics through reverse proxy (#463) 2026-06-01 09:29:30 +02:00
rule-builder-utils.test.ts feat(transactions): add counterparty fields (#440) 2026-05-27 16:20:55 +02:00
rule-builder-utils.ts feat: parent/child category tree (#474) 2026-06-03 19:30:12 +02:00
rule-engine-parity.test.ts refactor: consolidate duplicated financial calculations (#643) 2026-07-04 22:26:44 +02:00
rule-engine.ts feat(transactions): add counterparty fields (#440) 2026-05-27 16:20:55 +02:00
safe-storage.test.ts fix(ui): stop unusable web storage from white-screening the app (#743) 2026-08-09 15:33:22 +00:00
safe-storage.ts fix(ui): stop unusable web storage from white-screening the app (#743) 2026-08-09 15:33:22 +00:00
sankey-utils.ts Render the Cashflow Sankey with recharts (responsive + expense drill-down) (#670) 2026-07-12 16:26:17 +00:00
sentry.test.ts fix(observability): stop our own page navigations from reporting fake network errors (#780) 2026-08-11 16:12:00 +00:00
sentry.ts fix(observability): stop our own page navigations from reporting fake network errors (#780) 2026-08-11 16:12:00 +00:00
subscription-payment-issue-toast.test.ts feat: keep past due subscriptions active (#416) 2026-05-22 10:19:11 +01:00
subscription-payment-issue-toast.ts fix(observability): stop our own page navigations from reporting fake network errors (#780) 2026-08-11 16:12:00 +00:00
sync-manager.test.ts fix(sync): don't crash when IndexedDB is unavailable (PHP-LARAVEL-43) (#654) 2026-07-07 04:59:10 +00:00
sync-manager.ts fix(sync): don't crash when IndexedDB is unavailable (PHP-LARAVEL-43) (#654) 2026-07-07 04:59:10 +00:00
transaction-delete-confirmation.test.ts Fix Spanish translations in category and delete flows (#397) 2026-05-14 11:36:53 +01:00
transaction-delete-confirmation.ts Fix Spanish translations in category and delete flows (#397) 2026-05-14 11:36:53 +01:00
transaction-filter-serialization.ts feat(transactions): save and reuse transaction filters (#496) 2026-06-05 18:00:14 +02:00
transaction-re-evaluation.test.ts Add labels to automation rules (#379) 2026-05-11 14:56:25 +02:00
transaction-re-evaluation.ts Add labels to automation rules (#379) 2026-05-11 14:56:25 +02:00
utils.ts Prevent duplicate notes when re-evaluating automation rules 2025-12-04 16:42:54 +01:00