Skip to content

feat(debug): on-device console + richer ErrorBoundary for mobile crashes#5808

Merged
MarkusNeusinger merged 1 commit into
mainfrom
claude/fix-ios-crash-t1Sjz
May 6, 2026
Merged

feat(debug): on-device console + richer ErrorBoundary for mobile crashes#5808
MarkusNeusinger merged 1 commit into
mainfrom
claude/fix-ios-crash-t1Sjz

Conversation

@MarkusNeusinger
Copy link
Copy Markdown
Owner

Summary

Adds two surgical debugging affordances so we can diagnose mobile-only crashes (like the iOS regression in #5805) now and in the future, without needing a Mac + USB cable for Safari Remote Debugging.

  • app/index.html — Loads Eruda behind ?debug=1 so anyone (us or a user on a flaky device) can open https://anyplot.ai/?debug=1, get a full DevTools-style console, and capture the actual stack trace. Loaded as a classic <script> (not type="module") so it runs before module parse-time errors and can surface them — critical for the iOS case where the bundle itself may fail to parse.
  • app/src/components/ErrorBoundary.tsx — When the boundary trips, the user now sees:
    • The error message (always, not just in DEV — this is the part we actually need them to send us).
    • A "Show technical details" disclosure with the full stack, React component stack, navigator.userAgent, current URL, and an ISO timestamp.
    • A "Copy details" button using navigator.clipboard so users can paste the full bug report straight into chat or email. Falls back gracefully when the Clipboard API is unavailable (older iOS / insecure context) — the text is already visible in the disclosure.
  • ErrorBoundary.test.tsx — Two new tests cover the disclosure toggle and the clipboard copy flow.

Intentionally minimal: no new dependencies, no Sentry / external error tracking yet (worth a follow-up PR with explicit cost discussion), no changes to the existing DebugPage or other surfaces.

Why this matters

The boundary previously only showed the error message in import.meta.env.DEV, so a production user hitting a crash had nothing to give us. With these changes, the path from "user reports crash" to "we have a stack trace" is one screenshot or one paste — and for power users, ?debug=1 gives them a full console on the device.

Test plan

  • yarn test --run src/components/ErrorBoundary.test.tsx — 7/7 passing (5 existing + 2 new).
  • yarn test --run — full suite 466/466 passing.
  • yarn type-check — clean.
  • yarn lint on the changed files — no new errors (the 32 pre-existing errors in SpecsListPage.tsx, test-utils.tsx, and tests are unchanged by this PR).
  • yarn build — bundles cleanly.
  • Manual verification on a real iOS device once deployed: open https://anyplot.ai/?debug=1 and confirm Eruda's bug icon appears + console captures the actual error from App crashes into global ErrorBoundary on iOS (all browsers) #5805.
  • Manual verification of the boundary UI: trigger a render error in dev, confirm "Show technical details" reveals stack + UA + URL, and "Copy details" puts the same payload on the clipboard.

Follow-ups (out of scope)

  • Once Eruda gives us the iOS stack trace, fix the actual root cause in App crashes into global ErrorBoundary on iOS (all browsers) #5805 (likely Vite 8 build target or a transitive dep using regex lookbehind / modern syntax).
  • Evaluate Sentry or a similar lightweight error reporter for production so we don't depend on users manually copying details.

Closes part of #5805 (debugging tooling; the underlying crash fix is still TBD).


Generated by Claude Code

Mobile-only crashes (e.g. iOS WebKit incompatibilities) are hard to
diagnose without a stack trace and remote DevTools aren't available in
every browser. This adds two surgical debugging affordances so users
can capture and share what we need next time the app crashes on their
device — see #5805 for the iOS regression that motivated this.

- index.html: load Eruda behind ?debug=1 as a classic <script> so it
  runs before module parse-time errors and can surface them on-device.
- ErrorBoundary: always show the error message (not just in DEV), add a
  "Show technical details" disclosure with stack + React component
  stack + UA + URL + timestamp, and a "Copy details" button so users
  can paste a full bug report into chat or email.
- ErrorBoundary tests: cover the disclosure toggle and clipboard copy.
Copilot AI review requested due to automatic review settings May 6, 2026 16:51
@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@MarkusNeusinger MarkusNeusinger enabled auto-merge (squash) May 6, 2026 19:41
@MarkusNeusinger MarkusNeusinger disabled auto-merge May 6, 2026 19:42
@MarkusNeusinger MarkusNeusinger merged commit 7a09315 into main May 6, 2026
13 checks passed
@MarkusNeusinger MarkusNeusinger deleted the claude/fix-ios-crash-t1Sjz branch May 6, 2026 19:42
MarkusNeusinger added a commit that referenced this pull request May 6, 2026
## Summary

- iPhone users could not use the app: every iOS Safari (and CriOS, which
uses WebKit) instance threw `TypeError: window.requestIdleCallback is
not a function` at `AppDataProvider` mount, which `ErrorBoundary` caught
— leaving the user stuck on the error fallback. Surfaced by the
on-device debug console added in #5808.
- `requestIdleCallback` is documented as unsupported on Safari/iOS by
[caniuse](https://caniuse.com/requestidlecallback),
[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback),
and [WebKit bug 164193](https://bugs.webkit.org/show_bug.cgi?id=164193).
It exists only behind the *Experimental Features* toggle, which is off
by default — so this is a hard regression for **every** iOS visitor, not
a one-off device bug.
- Fix is an inline feature-detect in the existing `useEffect`: use
`requestIdleCallback` if present, otherwise fall back to `setTimeout(cb,
1)`. Cleanup picks the matching cancel function via captured boolean. No
new npm dependency, no global mutation.
- Adds a regression test that stubs both idle APIs as `undefined` and
asserts the three initial fetches still fire and children still render.

### Original iPhone error report

```
Message: window.requestIdleCallback is not a function. (In 'window.requestIdleCallback(async()=>{try{let[e,t,n]=await Promise.all([fetch(`${T}/specs`),fetch(`${T}/libraries`),fetch(`${T}/stats`)]);...},{timeout:2e3})')
URL: https://anyplot.ai/?debug=1
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 26_4_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/148.0.7778.100 Mobile/15E148 Safari/604.1
```

## Test plan

- [x] `cd app && yarn test --run` — 467/467 pass, including new `falls
back to setTimeout when requestIdleCallback is unavailable (iOS Safari)`
case in `Layout.test.tsx`.
- [x] `cd app && yarn build` — TypeScript / Vite build green.
- [x] Pre-existing lint findings on `main` unchanged; no new findings
introduced by this PR.
- [ ] Production verification on the reporter's iPhone after deploy
(visit https://anyplot.ai/?debug=1, confirm home page loads and the
Eruda console is clean).

## Why this approach (not a polyfill package)

- Two well-known polyfills exist
([aFarkas/requestIdleCallback](https://github.com/aFarkas/requestIdleCallback),
[pladaria/requestidlecallback-polyfill](https://github.com/pladaria/requestidlecallback-polyfill))
but both internally just wrap `setTimeout` — there is no way to truly
polyfill real browser idle time. Adding a dependency would ship bytes to
every modern-browser user for the same effect we get inline.
- Single call site in the entire repo — per CLAUDE.md "no abstractions
beyond what the task requires", a per-file inline fix is the right
shape. If a second call site appears later, that is the moment to
extract a `utils/idleCallback.ts`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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