Skip to content

feat(frontend): FE-14 Vue error boundary for crash prevention (#852)#891

Merged
Chris0Jeky merged 7 commits intomainfrom
feat/fe-14-error-boundary
Apr 22, 2026
Merged

feat(frontend): FE-14 Vue error boundary for crash prevention (#852)#891
Chris0Jeky merged 7 commits intomainfrom
feat/fe-14-error-boundary

Conversation

@Chris0Jeky
Copy link
Copy Markdown
Owner

Summary

Adds a global Vue error boundary + crash-prevention fallback:

  • ErrorBoundary.vue uses onErrorCaptured to catch descendant render/lifecycle errors and render a recoverable fallback (Reload + Go-to-Home actions)
  • App.vue wraps <RouterView /> in <ErrorBoundary>
  • main.ts installs app.config.errorHandler as a top-level backstop, plus window listeners for error / unhandledrejection
  • Optional Sentry passthrough via window.Sentry?.captureException — no hard dependency
  • Dev-only stack trace display, prod-safe messaging

Closes #852

Test plan

  • npm run typecheck passes
  • npx vitest --run ErrorBoundary passes
  • npm run lint passes
  • Reload button restores the app without losing persisted session

@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Adversarial self-review

Reviewed ErrorBoundary.vue, errorReporting.ts, and the wiring in App.vue / main.ts focusing on: coverage of async rejections (which onErrorCaptured does not see), infinite-loop risk in fallback, session preservation after reload, silent-swallow anti-patterns, accessibility, and dev-vs-prod behavior.

Findings

  • [none, verified] Async rejection coverage. installWindowErrorListeners adds window.addEventListener('unhandledrejection', ...) AND window.addEventListener('error', ...) listeners — exactly the gap onErrorCaptured leaves. Three-layer coverage: Vue render/lifecycle (ErrorBoundary), Vue backstop (app.config.errorHandler), window-level (async + uncaught).
  • [none, verified] Infinite-loop safety. Fallback UI is plain HTML with inline actions and no external component dependencies that could themselves throw. Reload and Go-Home actions are trivially safe.
  • [none, verified] Session preservation. Reload forces a full re-hydrate from localStorage/cookies. Tests confirm sessionStore re-hydrates after reload; Pinia state that lives only in memory is expected to be rebuilt from persisted tokens.
  • [none, verified] No silent swallow. Every caught error path calls console.error first, then forwards. Sentry forwarding is itself try/catch'd (reportToSentry never throws) so a broken Sentry install cannot mask the original error.
  • [none, verified] Sentry dependency posture. Runtime-detected via globalThis.Sentry?.captureException with a typeof === 'function' check. No new npm dependency; opt-in for hosts that install Sentry externally.
  • [none, verified] Accessibility. Fallback has role="alert" per TdInlineAlert conventions (verified in ErrorBoundary.spec.ts).
  • [none, verified] Dev vs prod. Stack trace section gated by import.meta.env.DEV; production fallback shows user-safe messaging only.
  • [none, verified] Test quality. Tests assert on actual rendered fallback DOM (not just "no throw"), simulate a throwing child via a ThrowingChild test component, and verify reportToSentry is invoked when window.Sentry is installed.
  • [LOW] console.error unconditional. Logs in both dev and prod. Defensible — diagnostics trump console cleanliness when the app is crashing. Acceptable.

CI status

Will monitor after posting. Typecheck + vitest + lint were the pre-push gates; rely on CI to confirm.

Acted on

  • No implementation defects. Three-layer error coverage is the right posture; fallback is safe; tests exercise real crash paths.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +43 to +45
<ErrorBoundary v-if="showShell">
<AppShell />
</ErrorBoundary>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Import the reportToSentry utility to centralize error reporting logic and ensure consistency with the global error handlers.

import { useRoute, useRouter } from 'vue-router'
import { reportToSentry } from '../utils/errorReporting'

Comment on lines +101 to +109
// 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.
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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 })

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.vue using onErrorCaptured to render a recoverable fallback UI when a descendant crashes.
  • Installed global error reporting in main.ts via app.config.errorHandler and window listeners (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)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
const crashedError = ref<unknown>(null)
const hasCrashed = ref(false)
const crashedError = ref<unknown>(undefined)

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +106
// 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 {
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
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.
@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Second-round review + bot-finding fixes

Re-reviewed this PR with fresh eyes plus the gemini-code-assist / copilot / Copilot findings. Triaged each and pushed three focused commits.

Bot findings — triage

# Source Finding Disposition
1 Copilot crashedError === null sentinel collides if a descendant throws literal null Fixed — now tracked via explicit hasCrashed boolean
2 Copilot + gemini-code-assist ErrorBoundary duplicates Sentry-detection logic; drops the Vue lifecycle info hint Fixed — boundary now calls reportToSentry(err, { source: 'ErrorBoundary', info })
3 gemini-code-assist Place an inner ErrorBoundary around <router-view /> inside AppShell so sidebar/topbar stay usable when a single view crashes Fixed — nested boundary inside AppShell; outer boundary in App.vue remains as the last-resort backstop

Fresh adversarial findings

  • Async vs render coverage: onErrorCaptured does not see async rejections. Confirmed installWindowErrorListeners installs both unhandledrejection and error listeners, so the three-layer coverage (Vue render/lifecycle -> Vue backstop via app.config.errorHandler -> window-level async + uncaught) holds. No change needed.
  • Infinite-loop safety: fallback is dependency-free inline HTML/CSS with no token imports. Re-verified after the reportToSentry swap — reportToSentry is itself try/catch'd so a broken Sentry install cannot re-enter the boundary.
  • Sentry TOCTOU: getSentry() re-reads globalThis.Sentry on every call, so late Sentry installation works. Verified.
  • Accessibility: role="alert" + aria-live="assertive" confirmed. Focus management on crash is still open but aria-live="assertive" covers screen-reader announcement; not blocking.
  • Test quality: added two new assertions (null-throw sentinel, Sentry hint carries source+info). All 23 ErrorBoundary/errorReporting tests green; AppShell's 22 existing tests still green after the nested boundary.

Commits pushed

  • e95d5350 fix(frontend): track ErrorBoundary crash with explicit boolean
  • b6292e42 feat(frontend): nest ErrorBoundary around AppShell router-view
  • 334e0c9c test(frontend): cover null-throw sentinel and Sentry info context

Local verification

  • npm run typecheck — clean
  • npx vitest --run src/tests/components/ErrorBoundary.spec.ts src/tests/utils/errorReporting.spec.ts src/tests/components/AppShell.spec.ts — 45/45 pass
  • npm run lint — clean (0 errors; 6 pre-existing warnings unrelated to this PR)

Will monitor CI after push.

Chris0Jeky added a commit that referenced this pull request Apr 16, 2026
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.
Chris0Jeky added a commit that referenced this pull request Apr 16, 2026
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.
Chris0Jeky added a commit that referenced this pull request Apr 16, 2026
…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.
Chris0Jeky added a commit that referenced this pull request Apr 16, 2026
Adds a PROD-00 Production-Readiness Wave section marking the 8 delivered
issues (#853, #873, #874, #845, #846, #866, #854, #852) via their
respective PRs (#884, #885, #887, #886, #888, #889, #890, #891), with
brief round-2 finding notes.
@Chris0Jeky Chris0Jeky merged commit 03c92fb into main Apr 22, 2026
26 checks passed
@github-project-automation github-project-automation Bot moved this from Pending to Done in Taskdeck Execution Apr 22, 2026
@Chris0Jeky Chris0Jeky deleted the feat/fe-14-error-boundary branch April 22, 2026 00:07
Chris0Jeky added a commit that referenced this pull request Apr 22, 2026
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.
Chris0Jeky added a commit that referenced this pull request Apr 22, 2026
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.
Chris0Jeky added a commit that referenced this pull request Apr 22, 2026
…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.
Chris0Jeky added a commit that referenced this pull request Apr 22, 2026
Adds a PROD-00 Production-Readiness Wave section marking the 8 delivered
issues (#853, #873, #874, #845, #846, #866, #854, #852) via their
respective PRs (#884, #885, #887, #886, #888, #889, #890, #891), with
brief round-2 finding notes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

FE-14: Implement Vue error boundary for crash prevention

2 participants