PURPOSE

Boot-time monkey-patch that wraps CanvasRenderingContext2D.prototype.createRadialGradient and createLinearGradient (plus the OffscreenCanvasRenderingContext2D equivalents) so non-finite coordinate/radius arguments substitute safe defaults instead of throwing TypeError: The provided double value is non-finite mid-frame. Defense-in-depth against the 50+ unguarded gradient callsites across bridge.ts, hud.ts, draw.ts, and vfx/*. Treats the Canvas API as an external boundary where defensive coding is permitted under the “crash on bad data, no silent fallbacks” rule.

OWNS

  • _installed boot-once flag.
  • CanvasGuardStats exported counter object (totalHits, reported).
  • _seenSignatures Set of stack-derived dedupe keys.
  • MAX_REPORTS Sentry-quota cap (3 per session).
  • Wrapped createRadialGradient / createLinearGradient prototype methods, tagged with __guarded to enforce idempotency.
  • window.__canvasGuard global handle (read-only counter exposure for devtools inspection).

READS FROM

  • Native CanvasRenderingContext2D.prototype and (when defined) OffscreenCanvasRenderingContext2D.prototype.
  • window.__dev / window.__diag — lazily reads getState() for game-state snapshot (phase, level, shipX, shipY) to avoid circular imports with the engine.
  • window.innerWidth, window.innerHeight, window.devicePixelRatio for viewport context.
  • new Error().stack for callsite dedupe signature.

PUSHES TO

  • logDiag (from ../telemetry/diag) with event_type: 'canvas_gradient_non_finite', level error, full payload (method name, raw args, fixed args, stack, captured context, counter snapshot). This is the Sentry + Supabase fan-out path.
  • The original (unguarded) gradient constructor on each invocation, after argument sanitization.
  • window.__canvasGuard global pointer.

DOES NOT

  • Throw. The wrappers never propagate errors; the inner _captureContext block swallows exceptions silently with a never throw comment.
  • Modify well-formed gradient calls — sanitized coords match originals when inputs are finite.
  • Patch other Canvas API methods (only the two gradient constructors).
  • Log to console. All diagnostic output flows through logDiag.
  • Import from game-state modules directly (deliberate, to avoid circular deps at boot).
  • Run in non-browser environments — guards SSR and test runners without jsdom by checking typeof CanvasRenderingContext2D === 'undefined'.
  • Re-wrap an already-guarded method (__guarded sentinel).

Signals

  • CanvasGuardStats.totalHits — incremented on every guard activation regardless of report state.
  • CanvasGuardStats.reported — incremented only when a unique stack signature is logged to logDiag. Capped at MAX_REPORTS = 3.
  • _seenSignatures membership — first four post-guard stack frames joined with | form the dedupe key; per-frame loops report at most once.
  • bug=canvas_gradient_non_finite Sentry tag (via logDiag payload event_type).
  • Payload fields surfaced on each report: method, rawArgs (coerced to numbers or stringified), fixedArgs, stack, phase, level, shipX, shipY, vpW, vpH, dpr, reportedCount, totalHits.

Entry points

  • installCanvasGradientGuards() — sole public function. Idempotent; intended to be called once at boot before any rendering.
  • CanvasGuardStats — exported live counter, mirrored to window.__canvasGuard.

Internal helpers (not exported): _safeCoord, _safeRadius, _captureContext, _report, _wrapRadial, _wrapLinear.

Pattern notes

  • Sanitization rules: coords coerced through Number(v); non-finite values become 0. Radii clamped to Math.max(min, n) where min is 0 for the inner radius and 0.001 for the outer; non-finite radii fall back to min. After substitution the wrapper enforces r1 >= r0 + 0.001 to dodge Chrome’s IndexSizeError on equal/inverted radii.
  • Reporting flow is gated three times: MAX_REPORTS quota check, stack-signature dedupe, then logDiag emission. totalHits increments before the quota gate so the counter reflects true activation count even when no report fires.
  • _captureContext is intentionally best-effort: it reads from window.__dev || window.__diag (whichever the bridge populated), wraps the getState() call in try { ... } catch { /* never throw */ }, and returns a partial object when fields are missing.
  • Prototype patching tags the replacement function with a non-enumerable-style __guarded property; subsequent installs short-circuit on detection. This makes installCanvasGradientGuards safe under hot-reload.
  • OffscreenCanvasRenderingContext2D guard exists to cover sprite-baking, atlas builder, and glow-stamp paths that may re-bake mid-run during a hot reload.
  • Stack signature uses stack.split('\n').slice(2, 6).join('|') — frames 0–1 (the guard itself) are dropped so the key reflects the calling code, not the guard.
  • Module-level _installed, _seenSignatures, and the MAX_REPORTS constant are private; only CanvasGuardStats and installCanvasGradientGuards cross the module boundary.