Canvas Gradient Guards
installCanvasGradientGuards() is a pre-mount safety bootstrap that monkey-patches CanvasRenderingContext2D.prototype.createRadialGradient and createLinearGradient (plus the OffscreenCanvasRenderingContext2D variants when available) so non-finite numeric inputs no longer crash the renderer. Source: src/starship-survivors/engine/diagnostics/canvas-guards.ts. Boot site: src/main.tsx.
Why it exists
Sentry issue STARSHIPSURVIVORS-Y (v5.163.2, 2026-05-03 09:56 UTC) caught TypeError: The provided double value is non-finite thrown from ctx.createRadialGradient(...) mid-frame. The bridge frame-error catch block kept rescheduling requestAnimationFrame, so the game appeared hard-frozen on the last good frame while audio (separate path) kept playing — exactly the user-reported symptom: blue-screen overlay, no transition, music keeps playing. After ~37s the user tapped RECOVER FROM STUCK, which tripped the known Rapier WASM “recursive use of an object” cleanup crash.
The codebase has 50+ direct calls to createRadialGradient / createLinearGradient across bridge.ts, hud.ts, draw.ts, and vfx/*. Most are unguarded against NaN / Infinity. The Canvas API is an external boundary — by project rules (“crash on bad data” applies inside the app; defensive coding belongs at boundaries) this is the right place to harden.
What it does
- Wraps the prototype methods on
CanvasRenderingContext2Dand (if present)OffscreenCanvasRenderingContext2D. The wrappers are tagged__guardedso a second install call is a no-op. - Sanitises args before delegating to the original method:
- Coordinates (
x0,y0,x1,y1) — non-finite values become0. - Inner radius
r0— non-finite or negative becomes0. - Outer radius
r1— non-finite or negative becomes0.001. - If
r0 >= r1after sanitisation,r1is bumped tor0 + 0.001to avoid Chrome’sIndexSizeError.
- Coordinates (
- Reports the first 3 unique-stack hits per session to Sentry + Supabase via
logDiagasevent_type: 'canvas_gradient_non_finite'. Payload includes the raw args, the substituted args, the JS stack, and a snapshot of likely culprits (game phase, level, ship X/Y, viewport width/height, devicePixelRatio) read offwindow.__dev/window.__diagif the engine populated them. Dedupe is by a hash of stack lines 2–6 so a per-frame loop reports once, not 60×/sec. - Exposes
window.__canvasGuard=CanvasGuardStats({totalHits,reported}) for live console inspection during a play test.
Net effect: any non-finite gradient input returns a valid (effectively transparent / degenerate) CanvasGradient instead of throwing, the renderer continues, and Sentry receives enough info to locate the producing callsite on the next iteration.
Boot order
installCanvasGradientGuards() runs as the first statement in src/main.tsx, before Sentry.init, before createRoot(...).render(<App />), and before any preload phase. This ordering matters: the patch must be active before the React tree mounts and before any module-init code touches a 2D context. Boot sequence in main.tsx:
installCanvasGradientGuards()— monkey-patch prototypes.Sentry.init({...})ifVITE_SENTRY_DSNis set, elselogDiag({ event_type: 'sentry_init_skipped' }).createRoot(document.getElementById('root')!).render(<App />).
If a future refactor reorders these, the guard becomes useless for any gradient created during module init.
Sentry quota
Cap is MAX_REPORTS = 3 per session, enforced before logDiag is called. Dedupe by stack signature happens before the counter increments, so three distinct callsites can each report once. totalHits keeps counting silently after that — the counter is visible at window.__canvasGuard.totalHits for in-session inspection but doesn’t burn Sentry budget.
Related
- Source:
src/starship-survivors/engine/diagnostics/canvas-guards.ts - Boot site:
src/main.tsx(line 13) - Telemetry sink:
src/starship-survivors/engine/telemetry/diag.ts(logDiag) - Sentry issue: STARSHIPSURVIVORS-Y (v5.163.2 freeze, 2026-05-03)
- Related symptom: bridge frame-error catch reschedules RAF on throw → silent freeze
- Downstream crash: Rapier WASM “recursive use of an object” on RECOVER FROM STUCK cleanup