fix(webapp): Evict legacy resizable-panel localStorage on client boot (#3564)
## Summary
- Users on production are hitting `QuotaExceededError: Failed to execute
'setItem' on 'Storage'` when navigating runs, because their localStorage
is full of orphaned `panel-group-react-aria<n>-:<rid>:` entries.
- Each entry is a session-unique key written by the resizable panel
library; they accumulated to thousands per user over the last two months
and now block legitimate `setItem` calls (the run-view inspector can no
longer persist its layout, and the page crashes mid-render).
- This PR evicts the legacy entries once on client boot. The leak itself
is already plugged by the v1.1.3 upgrade in #XXXX — this is the cleanup
that recovers the wasted quota on existing users' machines.
## Root cause (already fixed, for context)
In v0.4.1 of the underlying library, `PanelGroupImpl` defaulted
`autosaveStrategy` to `"localStorage"` unconditionally — so *every*
`PanelGroup` wrote to localStorage on every autosave trigger, including
the four in `QueryEditor`, the one in `ReplayRunDialog`, the storybook
routes, etc. Without an `autosaveId`, the key fell back to
`panel-group-${useId()}`, and React Aria's `useId()` produces a new
session-unique prefix each visit. Result: entries accumulated without
bound across sessions.
The condition was introduced when
[#3282](https://github.com/triggerdotdev/trigger.dev/pull/3282) removed
the wrapper's explicit `autosaveStrategy="cookie"` override (to fix HTTP
431 cookie-size errors). That worked, but the library default that took
over silently caused this leak.
The v1.1.3 upgrade in the resizable-panel PR changed the default to
`autosaveStrategy = autosaveId ? "localStorage" : undefined`, so no new
entries are being written. Existing residue still needs to be removed
from users' browsers.
## Changes
- New file
[`apps/webapp/app/clientBeforeFirstRender.ts`](apps/webapp/app/clientBeforeFirstRender.ts)
— exports a `clientBeforeFirstRender()` function that runs
synchronously, before React hydrates. Encapsulates a small cleanup
helper that scans `localStorage` and removes:
- Every key starting with `panel-group-react-aria` (the legacy
auto-generated keys).
- The orphan `panel-run-parent-v2` key from before the autosaveId v2→v3
bump.
- [`apps/webapp/app/entry.client.tsx`](apps/webapp/app/entry.client.tsx)
— imports and invokes `clientBeforeFirstRender()` once, before
`hydrateRoot()`. This guarantees the cleanup completes before any
`ResizablePanelGroup` mounts and tries to write.
The cleanup is wrapped in `try/catch` so private-browsing /
disabled-storage scenarios fail silently. Idempotent: subsequent loads
find no matching keys and exit immediately.
## Test plan
- [x] Locally seed ~50 fake `panel-group-react-aria…` entries plus a
`panel-run-parent-v2` entry via DevTools console, hard reload → legacy
entries gone, real entries (`panel-run-parent-v3`, `panel-run-tree`)
preserved.
- [x] Idempotency: reload a second time, no errors, no state changes.
- [x] Add a control entry (`panel-run-parent-v3-but-different-suffix`) —
confirmed not over-matched.
- [x] Simulate broken `Storage.setItem` throwing — page still renders,
cleanup swallows the error.
- [x] Typecheck clean.
## Notes
- Customer report: `QuotaExceededError: Failed to execute 'setItem' on
'Storage': Setting the value of 'panel-run-parent-v3' exceeded the
quota.`
- The cleanup runs once per page load. Once a user has loaded the app
after this deploys, their localStorage is clean and the function becomes
a no-op forever.
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Runs once on the client, synchronously, before React hydrates the app.
|
||||
* Reserved for housekeeping that must happen before any component mounts.
|
||||
*/
|
||||
export function clientBeforeFirstRender() {
|
||||
cleanupLegacyResizablePanelStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Earlier versions of the resizable panel library wrote a per-session
|
||||
* localStorage entry for every PanelGroup, including ones without an
|
||||
* `autosaveId`. The keys look like `panel-group-react-aria<n>-:<rid>:`
|
||||
* and accumulate without bound across sessions until they exhaust the
|
||||
* ~5 MB origin quota and break subsequent `setItem` calls.
|
||||
*
|
||||
* The library no longer behaves this way, but existing users still carry
|
||||
* the residue. Evict it (plus the orphaned `panel-run-parent-v2` key from
|
||||
* the v2→v3 autosaveId bump) once on load.
|
||||
*/
|
||||
function cleanupLegacyResizablePanelStorage() {
|
||||
try {
|
||||
const toRemove: string[] = [];
|
||||
for (let i = 0; i < window.localStorage.length; i++) {
|
||||
const key = window.localStorage.key(i);
|
||||
if (
|
||||
key &&
|
||||
(key.startsWith("panel-group-react-aria") || key === "panel-run-parent-v2")
|
||||
) {
|
||||
toRemove.push(key);
|
||||
}
|
||||
}
|
||||
for (const key of toRemove) {
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
} catch {
|
||||
// localStorage may be disabled (private browsing, security policy)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import { RemixBrowser } from "@remix-run/react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import { clientBeforeFirstRender } from "./clientBeforeFirstRender";
|
||||
import { LocaleContextProvider } from "./components/primitives/LocaleProvider";
|
||||
import { OperatingSystemContextProvider } from "./components/primitives/OperatingSystemProvider";
|
||||
|
||||
clientBeforeFirstRender();
|
||||
|
||||
hydrateRoot(
|
||||
document,
|
||||
<OperatingSystemContextProvider
|
||||
|
||||
Reference in New Issue
Block a user