feat(frontend): FE-14 Vue error boundary for crash prevention (#852)#891
feat(frontend): FE-14 Vue error boundary for crash prevention (#852)#891Chris0Jeky merged 7 commits intomainfrom
Conversation
Adversarial self-reviewReviewed Findings
CI statusWill monitor after posting. Typecheck + vitest + lint were the pre-push gates; rely on CI to confirm. Acted on
|
There was a problem hiding this comment.
Code Review
This pull request introduces a centralized error handling and reporting system, including a new ErrorBoundary component and global listeners for unhandled rejections. The main application layout is now protected by these boundaries to prevent full-page crashes. Review feedback suggests improving the user experience by placing error boundaries more granularly within the application shell and refactoring the ErrorBoundary component to use the centralized reporting utility for better consistency and maintainability.
| <ErrorBoundary v-if="showShell"> | ||
| <AppShell /> | ||
| </ErrorBoundary> |
There was a problem hiding this comment.
Wrapping the entire AppShell in an ErrorBoundary is a robust top-level backstop. However, for a better user experience, consider also placing an ErrorBoundary inside AppShell.vue specifically around the <router-view /> (line 194). This would allow the sidebar and topbar to remain functional if only a specific page component crashes, enabling the user to navigate to other parts of the app without a full recovery action.
| * `main.ts`. | ||
| */ | ||
| import { onErrorCaptured, ref, watch } from 'vue' | ||
| import { useRoute, useRouter } from 'vue-router' |
| // Forward to Sentry if the host page has installed it (no hard dependency). | ||
| const sentry = (globalThis as unknown as { Sentry?: { captureException?: (e: unknown) => void } }).Sentry | ||
| if (sentry && typeof sentry.captureException === 'function') { | ||
| try { | ||
| sentry.captureException(err) | ||
| } catch { | ||
| // Never let reporting failures bubble out of the boundary. | ||
| } | ||
| } |
There was a problem hiding this comment.
This manual Sentry reporting logic is redundant with the reportToSentry utility. Using the utility ensures that the info parameter (which contains the lifecycle hook context) is correctly passed to Sentry and simplifies future updates to the reporting logic.
// Forward to Sentry using the centralized utility.
reportToSentry(err, { info })
There was a problem hiding this comment.
Pull request overview
Implements a global crash-prevention layer for the Vue frontend by adding an application-wide error boundary component plus centralized error reporting hooks (Vue + window-level), with accompanying unit tests.
Changes:
- Added
ErrorBoundary.vueusingonErrorCapturedto render a recoverable fallback UI when a descendant crashes. - Installed global error reporting in
main.tsviaapp.config.errorHandlerandwindowlisteners (error/unhandledrejection). - Added Vitest coverage for both the error boundary UI behavior and the reporting utilities.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/taskdeck-web/src/utils/errorReporting.ts | Adds shared error reporting helpers (Vue errorHandler + window listeners + optional Sentry passthrough). |
| frontend/taskdeck-web/src/main.ts | Installs the global Vue and window error handlers before mounting the app. |
| frontend/taskdeck-web/src/components/ErrorBoundary.vue | Introduces a global error boundary component with fallback UI and recovery actions. |
| frontend/taskdeck-web/src/App.vue | Wraps AppShell / router-view rendering with the new ErrorBoundary. |
| frontend/taskdeck-web/src/tests/utils/errorReporting.spec.ts | Adds unit tests for the error reporting utilities. |
| frontend/taskdeck-web/src/tests/components/ErrorBoundary.spec.ts | Adds unit tests validating fallback UI, reset behavior, and Sentry passthrough. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| reset: [] | ||
| }>() | ||
|
|
||
| const crashedError = ref<unknown>(null) |
There was a problem hiding this comment.
crashedError uses null as the sentinel for “no crash”, but Vue errors can be any thrown value (including null). If a descendant throws null, the boundary will treat it as healthy (crashedError === null) and never render the fallback. Use an explicit hasCrashed boolean (or a dedicated sentinel value) instead of null to track crash state, and store the error separately.
| const crashedError = ref<unknown>(null) | |
| const hasCrashed = ref(false) | |
| const crashedError = ref<unknown>(undefined) |
| // Forward to Sentry if the host page has installed it (no hard dependency). | ||
| const sentry = (globalThis as unknown as { Sentry?: { captureException?: (e: unknown) => void } }).Sentry | ||
| if (sentry && typeof sentry.captureException === 'function') { | ||
| try { | ||
| sentry.captureException(err) | ||
| } catch { |
There was a problem hiding this comment.
ErrorBoundary is duplicating the Sentry passthrough logic instead of reusing reportToSentry from src/utils/errorReporting.ts, and it drops useful context like the Vue info string. Consider importing and calling reportToSentry(err, { source: 'ErrorBoundary', info }) here so Sentry reporting behavior stays consistent across the app.
Previously the boundary used `crashedError === null` as the healthy sentinel. Since Vue errors can be any thrown value (including `null`), a descendant that threw `null` would be treated as healthy and the fallback would never render. Track crash state with a dedicated `hasCrashed` boolean and store the error separately. Also route Sentry forwarding through the centralized `reportToSentry` utility so the Vue lifecycle `info` string and the `source` tag are preserved, eliminating duplicated Sentry-detection logic.
Wrapping `<router-view />` inside `AppShell` with its own ErrorBoundary keeps the sidebar and topbar functional when a single route component crashes: the user can navigate elsewhere without a full-page reload. The outer boundary in `App.vue` remains as the last-resort backstop for crashes in `AppShell` itself.
- Add a test where a descendant throws `null` to lock in the explicit `hasCrashed` boolean sentinel — this guards against future regressions where a falsy sentinel could make the fallback invisible. - Tighten the Sentry passthrough test to assert the hint carries `source: 'ErrorBoundary'` and the Vue lifecycle `info` string, so the centralized reporting contract is verified end-to-end from the boundary.
Second-round review + bot-finding fixesRe-reviewed this PR with fresh eyes plus the gemini-code-assist / copilot / Copilot findings. Triaged each and pushed three focused commits. Bot findings — triage
Fresh adversarial findings
Commits pushed
Local verification
Will monitor CI after push. |
Adds a delivery section for PRs #884-#891 covering SEC-28 SECURITY.md, DOC-06 CONTRIBUTING.md, DOC-07 CONFIGURATION_REFERENCE.md, PERF-09 response compression (with BREACH-aware level downgrade), PERF-10 composite DB indexes, OPS-29 container hardening (non-root end-to-end, IPv6/IPv4 healthcheck fix, setpriv entrypoint), FE-14 error boundary (null-throw sentinel fix), and FE-15 HTTP retry with backoff (skipRetry opt-out). Updates backend/frontend snapshots to reflect the new hardening surfaces.
Adds a delivery entry for the 8 PROD-00 PRs merged on 2026-04-16 (#884 SEC-28, #885 DOC-06, #887 DOC-07, #886 PERF-09, #888 PERF-10, #889 OPS-29, #890 FE-15, #891 FE-14) with round-2 adversarial review findings: BREACH JWT-in-body correction (compression level Optimal -> Fastest), IPv6/IPv4 healthcheck fix, null-throw sentinel fix, skipRetry opt-out for baseline tests, setpriv entrypoint for upgrade-safe volume ownership. Also bumps the Last Updated date.
…layer error coverage Adds a PROD-00 Production-Readiness Round-2 Wave section covering: - ResponseCompressionApiTests (#886, +3 tests) - migration-only context for composite DB indexes (#888) - container hardening verification (no unit tests, docker inspect path) - HTTP retry with backoff tests + skipRetry opt-out pattern for future test authors (#890) - ErrorBoundary + errorReporting tests + three-layer error coverage pattern documenting outer/inner/window layers (#891) Updates Current Verified Totals to reflect the new test deltas.
Adds a delivery section for PRs #884-#891 covering SEC-28 SECURITY.md, DOC-06 CONTRIBUTING.md, DOC-07 CONFIGURATION_REFERENCE.md, PERF-09 response compression (with BREACH-aware level downgrade), PERF-10 composite DB indexes, OPS-29 container hardening (non-root end-to-end, IPv6/IPv4 healthcheck fix, setpriv entrypoint), FE-14 error boundary (null-throw sentinel fix), and FE-15 HTTP retry with backoff (skipRetry opt-out). Updates backend/frontend snapshots to reflect the new hardening surfaces.
Adds a delivery entry for the 8 PROD-00 PRs merged on 2026-04-16 (#884 SEC-28, #885 DOC-06, #887 DOC-07, #886 PERF-09, #888 PERF-10, #889 OPS-29, #890 FE-15, #891 FE-14) with round-2 adversarial review findings: BREACH JWT-in-body correction (compression level Optimal -> Fastest), IPv6/IPv4 healthcheck fix, null-throw sentinel fix, skipRetry opt-out for baseline tests, setpriv entrypoint for upgrade-safe volume ownership. Also bumps the Last Updated date.
…layer error coverage Adds a PROD-00 Production-Readiness Round-2 Wave section covering: - ResponseCompressionApiTests (#886, +3 tests) - migration-only context for composite DB indexes (#888) - container hardening verification (no unit tests, docker inspect path) - HTTP retry with backoff tests + skipRetry opt-out pattern for future test authors (#890) - ErrorBoundary + errorReporting tests + three-layer error coverage pattern documenting outer/inner/window layers (#891) Updates Current Verified Totals to reflect the new test deltas.
Summary
Adds a global Vue error boundary + crash-prevention fallback:
ErrorBoundary.vueusesonErrorCapturedto catch descendant render/lifecycle errors and render a recoverable fallback (Reload + Go-to-Home actions)App.vuewraps<RouterView />in<ErrorBoundary>main.tsinstallsapp.config.errorHandleras a top-level backstop, pluswindowlisteners forerror/unhandledrejectionwindow.Sentry?.captureException— no hard dependencyCloses #852
Test plan
npm run typecheckpassesnpx vitest --run ErrorBoundarypassesnpm run lintpasses