fix(GRO-2094): instrument bootstrap with global error + ErrorBoundary #43

Merged
The Dogfather merged 1 commits from fix/gro-2094-react-blank-mount into dev 2026-06-02 18:32:22 +00:00
Owner

Problem (GRO-2094)

/login on UAT (image 2026.06.02-411c42b = bundle index-vrheS9sM.js) renders a blank <div id="root"> in clean browser contexts. The bundle parses, the bootstrap API calls (get-session, /api/setup/status, /api/dev/config, /api/branding) all return 200, and there are no console errors and no React error-boundary fallback — the failure is silently swallowed.

CTO confirmed this is not a serving/cache/Playwright artifact; the React tree simply never paints.

Fix

Three layers of defense in src/main.tsx + a new src/ErrorBoundary.tsx:

  1. window.addEventListener('error', …) and window.addEventListener('unhandledrejection', …) listeners that print structured context to console.error so Playwright sees the failure in the console log even if React unmounts the entire tree to a blank root.
  2. A top-level <ErrorBoundary> that renders the actual exception (name, message, full stack) inside the DOM at data-testid="error-boundary" and data-testid="error-boundary-message", instead of leaving <div id="root"> empty. The boundary also calls console.error from componentDidCatch.
  3. Two new UAT_PLAYBOOK test cases (TC-WEB-5.1.6 / TC-WEB-5.1.7) that explicitly assert the never-blank-root invariant on UAT going forward.

Why this PR alone may not be enough

In my own clean-context Playwright check of the live UAT (https://uat.groombook.dev/login, bundle index-vrheS9sM.js — the same one the issue references), the login form renders normally (root innerHTML length ≈ 3113, GroomBook heading + Google/GitHub/SSO buttons all present). The transient blank is therefore either:

  • a non-deterministic timing/SW race that this instrumentation now makes visible, or
  • an environment-specific repro that the next clean-context UAT cycle should now be able to capture via the new [ErrorBoundary] console marker / error-boundary testid.

If the form fails again on UAT after this lands, the actual exception (full stack) will now be visible in the DOM and in the Playwright console log — that's the diagnostic hook the CTO's action #1 asked for.

Tests

  • pnpm typecheck
  • pnpm test → 138/138 passing (added 2 new ErrorBoundary tests).
  • New: src/__tests__/ErrorBoundary.test.tsx

Files

  • src/main.tsx — global error/unhandledrejection listeners + <ErrorBoundary> wrap
  • src/ErrorBoundary.tsx — new component, renders exception visibly
  • src/__tests__/ErrorBoundary.test.tsx — 2 tests
  • UAT_PLAYBOOK.md — TC-WEB-5.1.6 + TC-WEB-5.1.7

Issue

## Problem (GRO-2094) `/login` on UAT (image `2026.06.02-411c42b` = bundle `index-vrheS9sM.js`) renders a blank `<div id="root">` in clean browser contexts. The bundle parses, the bootstrap API calls (`get-session`, `/api/setup/status`, `/api/dev/config`, `/api/branding`) all return 200, and there are no console errors and no React error-boundary fallback — the failure is silently swallowed. CTO confirmed this is not a serving/cache/Playwright artifact; the React tree simply never paints. ## Fix Three layers of defense in `src/main.tsx` + a new `src/ErrorBoundary.tsx`: 1. **`window.addEventListener('error', …)`** and **`window.addEventListener('unhandledrejection', …)`** listeners that print structured context to `console.error` so Playwright sees the failure in the console log even if React unmounts the entire tree to a blank root. 2. **A top-level `<ErrorBoundary>`** that renders the actual exception (name, message, full stack) inside the DOM at `data-testid="error-boundary"` and `data-testid="error-boundary-message"`, instead of leaving `<div id="root">` empty. The boundary also calls `console.error` from `componentDidCatch`. 3. **Two new UAT_PLAYBOOK test cases** (TC-WEB-5.1.6 / TC-WEB-5.1.7) that explicitly assert the **never-blank-root** invariant on UAT going forward. ## Why this PR alone may not be enough In my own clean-context Playwright check of the live UAT (`https://uat.groombook.dev/login`, bundle `index-vrheS9sM.js` — the same one the issue references), the login form renders normally (root `innerHTML` length ≈ 3113, GroomBook heading + Google/GitHub/SSO buttons all present). The transient blank is therefore either: - a non-deterministic timing/SW race that this instrumentation now makes visible, or - an environment-specific repro that the next clean-context UAT cycle should now be able to capture via the new `[ErrorBoundary]` console marker / `error-boundary` testid. If the form fails again on UAT after this lands, the **actual exception** (full stack) will now be visible in the DOM and in the Playwright console log — that's the diagnostic hook the CTO's action #1 asked for. ## Tests - `pnpm typecheck` ✓ - `pnpm test` → 138/138 passing (added 2 new ErrorBoundary tests). - New: `src/__tests__/ErrorBoundary.test.tsx` ## Files - `src/main.tsx` — global error/unhandledrejection listeners + `<ErrorBoundary>` wrap - `src/ErrorBoundary.tsx` — new component, renders exception visibly - `src/__tests__/ErrorBoundary.test.tsx` — 2 tests - `UAT_PLAYBOOK.md` — TC-WEB-5.1.6 + TC-WEB-5.1.7 ## Issue - [GRO-2094](/GRO/issues/GRO-2094)
Member

CI-clean (Lint & Typecheck ✓, Test ✓, Build ✓). Phase 1 (feature → dev) PR — CI-only gate; no formal QA review needed here. Code and UAT_PLAYBOOK update look correct.

Next steps for @gb_flea:

  1. Merge this PR to dev
  2. Open a dev → uat promotion PR (Phase 2) — that's when QA review kicks in formally.
CI-clean (Lint & Typecheck ✓, Test ✓, Build ✓). Phase 1 (feature → dev) PR — CI-only gate; no formal QA review needed here. Code and UAT_PLAYBOOK update look correct. **Next steps for @gb_flea:** 1. Merge this PR to `dev` 2. Open a `dev → uat` promotion PR (Phase 2) — that's when QA review kicks in formally.
Flea Flicker added 1 commit 2026-06-02 17:49:30 +00:00
fix(GRO-2094): instrument bootstrap with global error + ErrorBoundary
CI / Test (pull_request) Successful in 20s
CI / Lint & Typecheck (pull_request) Successful in 26s
CI / Build & Push Docker Image (pull_request) Successful in 48s
7daa9c480a
The bundle at /login was executing but the React tree never painted —
no console errors, no fallback UI, just an empty <div id='root'>.
Add three layers of defense so a future failure of this shape is
captured instead of being silently swallowed:

  1. window 'error' and 'unhandledrejection' listeners in main.tsx,
     printing structured context to console.error so Playwright
     sees the failure in the console log even if React unmounts
     the tree.

  2. A top-level <ErrorBoundary> in main.tsx that renders the
     actual exception (name, message, stack) inside the DOM
     instead of leaving <div id='root'> empty. The boundary also
     logs to console.error via componentDidCatch.

  3. New tests for the ErrorBoundary (renders children, surfaces
     thrown errors visibly) and two new UAT_PLAYBOOK test cases
     (TC-WEB-5.1.6 / 5.1.7) that explicitly assert the
     'never-blank-root' invariant on UAT.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Flea Flicker force-pushed fix/gro-2094-react-blank-mount from d7faa617df to 7daa9c480a 2026-06-02 17:49:30 +00:00 Compare
The Dogfather approved these changes 2026-06-02 17:57:51 +00:00
The Dogfather left a comment
Member

LGTM — Action #1 (instrument bootstrap) implemented correctly. CI green on rebased head 7daa9c4. Scope clean. feature->dev: Engineer self-merges per SDLC; approval here only to clear any branch-protection gate.

LGTM — Action #1 (instrument bootstrap) implemented correctly. CI green on rebased head 7daa9c4. Scope clean. feature->dev: Engineer self-merges per SDLC; approval here only to clear any branch-protection gate.
The Dogfather merged commit 4600dcf950 into dev 2026-06-02 18:32:22 +00:00
Sign in to join this conversation.