feat(debug): on-device console + richer ErrorBoundary for mobile crashes#5808
Merged
Conversation
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.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
4 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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=1so anyone (us or a user on a flaky device) can openhttps://anyplot.ai/?debug=1, get a full DevTools-style console, and capture the actual stack trace. Loaded as a classic<script>(nottype="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:navigator.userAgent, current URL, and an ISO timestamp.navigator.clipboardso 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
DebugPageor 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=1gives 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 linton the changed files — no new errors (the 32 pre-existing errors inSpecsListPage.tsx,test-utils.tsx, and tests are unchanged by this PR).yarn build— bundles cleanly.https://anyplot.ai/?debug=1and confirm Eruda's bug icon appears + console captures the actual error from App crashes into global ErrorBoundary on iOS (all browsers) #5805.Follow-ups (out of scope)
Closes part of #5805 (debugging tooling; the underlying crash fix is still TBD).
Generated by Claude Code