Skip to content

fix(webapp): Evict legacy resizable-panel localStorage on client boot#3564

Open
samejr wants to merge 2 commits into
mainfrom
fix(webapp)-evict-legacy-resizable-panel-localstorage
Open

fix(webapp): Evict legacy resizable-panel localStorage on client boot#3564
samejr wants to merge 2 commits into
mainfrom
fix(webapp)-evict-legacy-resizable-panel-localstorage

Conversation

@samejr
Copy link
Copy Markdown
Member

@samejr samejr commented May 12, 2026

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 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 — 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 — 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

  • 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.
  • Idempotency: reload a second time, no errors, no state changes.
  • Add a control entry (panel-run-parent-v3-but-different-suffix) — confirmed not over-matched.
  • Simulate broken Storage.setItem throwing — page still renders, cleanup swallows the error.
  • 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.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 12, 2026

⚠️ No Changeset found

Latest commit: 8d98c48

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Review Change Stack

Walkthrough

This PR introduces a pre-hydration cleanup mechanism to remove accumulated legacy localStorage keys used by the resizable panel library. A new clientBeforeFirstRender() function is created to scan window.localStorage and remove keys matching panel-group-react-aria* and panel-run-parent-v2 patterns, with error handling for cases where localStorage is unavailable. This function is then imported and invoked in entry.client.tsx immediately before React hydration begins, ensuring cleanup occurs once on client load without affecting the existing Remix component tree.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: evicting legacy resizable-panel localStorage entries on client boot, which directly addresses the root problem of quota exceeded errors.
Description check ✅ Passed The description is comprehensive and well-structured, covering the problem, root cause, solution details, and extensive test plan. It follows most of the template structure with clear sections for Summary, Changes, and Test plan.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix(webapp)-evict-legacy-resizable-panel-localstorage

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/webapp/app/clientBeforeFirstRender.ts (1)

20-38: 💤 Low value

Consider a more precise pattern to match legacy keys.

The current startsWith("panel-group-react-aria") check will match any key beginning with that prefix. According to the PR description, legacy keys follow the specific pattern panel-group-react-aria<n>-:<rid>: (with a digit after the prefix and colon delimiters). While your testing confirmed that current entries are preserved, a more precise regex like /^panel-group-react-aria\d+-:/ would defensively guard against accidentally removing a future autosaveId that happens to start with the same prefix.

🛡️ More defensive pattern matching
 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")
+        (/^panel-group-react-aria\d+-:/.test(key) || key === "panel-run-parent-v2")
       ) {
         toRemove.push(key);
       }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/clientBeforeFirstRender.ts` around lines 20 - 38, The
key-matching in cleanupLegacyResizablePanelStorage is too broad (uses
key.startsWith("panel-group-react-aria")); narrow it to the legacy pattern by
testing keys with a stricter regex (e.g., /^panel-group-react-aria\d+-:/) before
adding them to toRemove so only keys like panel-group-react-aria<n>-:<rid>: are
removed; update the check in cleanupLegacyResizablePanelStorage to use this
regex test instead of startsWith while keeping the existing fallback for
"panel-run-parent-v2" and the try/catch behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/webapp/app/clientBeforeFirstRender.ts`:
- Around line 20-38: The key-matching in cleanupLegacyResizablePanelStorage is
too broad (uses key.startsWith("panel-group-react-aria")); narrow it to the
legacy pattern by testing keys with a stricter regex (e.g.,
/^panel-group-react-aria\d+-:/) before adding them to toRemove so only keys like
panel-group-react-aria<n>-:<rid>: are removed; update the check in
cleanupLegacyResizablePanelStorage to use this regex test instead of startsWith
while keeping the existing fallback for "panel-run-parent-v2" and the try/catch
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a29cfc4d-9df0-41e5-a739-cbadacf1126a

📥 Commits

Reviewing files that changed from the base of the PR and between 8e675a4 and 8d98c48.

📒 Files selected for processing (2)
  • apps/webapp/app/clientBeforeFirstRender.ts
  • apps/webapp/app/entry.client.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: typecheck / typecheck
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

Files:

  • apps/webapp/app/clientBeforeFirstRender.ts
  • apps/webapp/app/entry.client.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/clientBeforeFirstRender.ts
  • apps/webapp/app/entry.client.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • apps/webapp/app/clientBeforeFirstRender.ts
  • apps/webapp/app/entry.client.tsx
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • apps/webapp/app/clientBeforeFirstRender.ts
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: Access environment variables through the env export of env.server.ts instead of directly accessing process.env
Use subpath exports from @trigger.dev/core package instead of importing from the root @trigger.dev/core path

Use named constants for sentinel/placeholder values (e.g. const UNSET_VALUE = '__unset__') instead of raw string literals scattered across comparisons

Files:

  • apps/webapp/app/clientBeforeFirstRender.ts
  • apps/webapp/app/entry.client.tsx
{apps,internal-packages}/**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Use pnpm run typecheck to verify changes in apps and internal packages (apps/*, internal-packages/*) instead of build, which proves almost nothing about correctness

Files:

  • apps/webapp/app/clientBeforeFirstRender.ts
  • apps/webapp/app/entry.client.tsx
{package.json,**/*.{ts,tsx,js}}

📄 CodeRabbit inference engine (CLAUDE.md)

Pin Zod to version 3.25.76 exactly across the entire monorepo - never use a different version or version range

Files:

  • apps/webapp/app/clientBeforeFirstRender.ts
  • apps/webapp/app/entry.client.tsx
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js}: Import from @trigger.dev/core using subpaths only, never the root export
Always import tasks from @trigger.dev/sdk, never from @trigger.dev/sdk/v3 or deprecated client.defineJob
Add crumbs to code using // @Crumbs comments or `// `#region` `@crumbs blocks for debug tracing during development

Files:

  • apps/webapp/app/clientBeforeFirstRender.ts
  • apps/webapp/app/entry.client.tsx
**/*.{ts,tsx,js,jsx,json,md,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Code formatting is enforced using Prettier. Run pnpm run format before committing

Files:

  • apps/webapp/app/clientBeforeFirstRender.ts
  • apps/webapp/app/entry.client.tsx
apps/webapp/**/*.{tsx,jsx}

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

Only use useCallback/useMemo for context provider values, expensive derived data that is a dependency elsewhere, or stable refs required by a dependency array. Don't wrap ordinary event handlers or trivial computations

Files:

  • apps/webapp/app/entry.client.tsx
🧠 Learnings (4)
📚 Learning: 2026-03-22T13:26:12.060Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3244
File: apps/webapp/app/components/code/TextEditor.tsx:81-86
Timestamp: 2026-03-22T13:26:12.060Z
Learning: In the triggerdotdev/trigger.dev codebase, do not flag `navigator.clipboard.writeText(...)` calls for `missing-await`/`unhandled-promise` issues. These clipboard writes are intentionally invoked without `await` and without `catch` handlers across the project; keep that behavior consistent when reviewing TypeScript/TSX files (e.g., usages like in `apps/webapp/app/components/code/TextEditor.tsx`).

Applied to files:

  • apps/webapp/app/clientBeforeFirstRender.ts
  • apps/webapp/app/entry.client.tsx
📚 Learning: 2026-03-22T19:24:14.403Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3187
File: apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts:200-204
Timestamp: 2026-03-22T19:24:14.403Z
Learning: In the triggerdotdev/trigger.dev codebase, webhook URLs are not expected to contain embedded credentials/secrets (e.g., fields like `ProjectAlertWebhookProperties` should only hold credential-free webhook endpoints). During code review, if you see logging or inclusion of raw webhook URLs in error messages, do not automatically treat it as a credential-leak/secrets-in-logs issue by default—first verify the URL does not contain embedded credentials (for example, no username/password in the URL, no obvious secret/token query params or fragments). If the URL is credential-free per this project’s conventions, allow the logging.

Applied to files:

  • apps/webapp/app/clientBeforeFirstRender.ts
  • apps/webapp/app/entry.client.tsx
📚 Learning: 2026-02-11T16:37:32.429Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3019
File: apps/webapp/app/components/primitives/charts/Card.tsx:26-30
Timestamp: 2026-02-11T16:37:32.429Z
Learning: In projects using react-grid-layout, avoid relying on drag-handle class to imply draggability. Ensure drag-handle elements only affect dragging when the parent grid item is configured draggable in the layout; conditionally apply cursor styles based on the draggable prop. This improves correctness and accessibility.

Applied to files:

  • apps/webapp/app/entry.client.tsx
📚 Learning: 2026-05-08T21:00:20.973Z
Learnt from: samejr
Repo: triggerdotdev/trigger.dev PR: 3538
File: apps/webapp/app/components/primitives/Resizable.tsx:60-78
Timestamp: 2026-05-08T21:00:20.973Z
Learning: In the triggerdotdev/trigger.dev codebase, treat Zod as a boundary validation tool (API handlers, request/response validation, and storage/DB read/write validation), not as inline render-time validation inside React components/primitive UI code. For render-time guards, prefer small manual type-narrowing checks (e.g., a short predicate like ~10–20 lines) over importing Zod into UI primitives, to avoid per-render schema-parse overhead and unnecessary abstraction. Use the manual guard approach unless you truly need schema validation at a boundary; only then introduce Zod.

Applied to files:

  • apps/webapp/app/entry.client.tsx
🔇 Additional comments (2)
apps/webapp/app/clientBeforeFirstRender.ts (1)

5-7: LGTM!

apps/webapp/app/entry.client.tsx (1)

3-3: LGTM!

Also applies to: 7-8

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants