import InfoBox from '../components/InfoBox.astro';

Until now, nobody found out when something broke in a player's browser. A crash mid-game, a failed leaderboard submission, a stuck PvP turn: the only way any of it surfaced was a player bothering to report it, and as the sole maintainer of a hobby project, that left real gaps invisible. Client-side error monitoring closes that gap, in error-only mode, with no Session Replay and no performance tracing.

The mode matters because the app is deliberately cookie-free. The privacy policy states plainly that no cookies are used, and the existing analytics tool was picked specifically because it needs no consent banner. A monitoring tool that introduced tracking cookies would have meant building consent infrastructure that doesn't exist anywhere in this codebase for that alone, so error-only Sentry was the fit: no cookies, no consent banner, and no change to a privacy stance that had already shaped an earlier, shelved attempt at marketing analytics. The alternative was self-hosting a bespoke error endpoint and database. For a solo-maintained project, a third-party service that already solves quota limits, deduplication, and email alerting beat building and running the equivalent machinery from scratch.

Getting the SDK to load safely took its own care. `@sentry/sveltekit` reaches statically into SvelteKit's app stores in a way that only resolves inside a full SvelteKit context, and the environment module it needs isn't populated in Storybook. A top-level import of either broke nine unrelated test and story files that merely happened to import the error-reporting module transitively, without ever triggering an error report. Both imports moved inside the functions that actually need them, keeping the module itself side-effect-free to import from anywhere.

<InfoBox title="What leaves the browser, what never does">
Every report carries a generalized route (a PvP game's ID is stripped to a placeholder before it's sent), the app version, the player's language, and the game mode. Words a player typed, their leaderboard initials, and any Duel display name are stripped from breadcrumbs and extra data as a defensive backstop, on top of restricting the SDK's own click and input breadcrumbs to element selectors only. Repeated identical errors are deduplicated in the browser before they're ever sent, so one bad deploy can't burn through a month's event quota in minutes.
</InfoBox>

## The first real error

The first error the new pipeline reported was not a contrived test. It surfaced on the very preview environment being used to verify the feature, within hours of the code landing there: an uncaught `Failed to fetch dynamically imported module` failure, thrown when a client-side navigation tried to load a route chunk by a hash that a redeploy had already replaced. It is not specific to preview environments either; it can happen in production any time a deploy lands while a player still has the app open in a tab and then navigates.

Vite fires its own `vite:preloadError` event for exactly this failure, in both development and the built output, which made the fix a matter of listening for it rather than inferring the failure from a generic uncaught exception:

```typescript
window.addEventListener('vite:preloadError', (event) => {
	event.preventDefault();
	const payload = (event as Event & { payload?: unknown }).payload;
	reportError(
		payload instanceof Error ? payload : new Error('Stale build: dynamic import failed'),
		{ category: 'stale-build' }
	);
	if (shouldReloadForStaleBuild(sessionStorage)) {
		setTimeout(() => window.location.reload(), 300);
	}
});
```

The failure gets tagged as its own `stale-build` category, distinct from a plain uncaught exception, then self-heals with a single reload. That reload is gated by a one-shot flag in session storage, the same loop-guard shape the daily challenge page already uses for its own stale-version check, so a second failure in the same browser session (an unrefreshed caching proxy still serving the old chunk, say) doesn't reload forever.

Confirming the fix actually stops the error from recurring is manual work for whenever the next few days of real traffic settle: watch the Sentry project, and once the count stops climbing, mark it resolved there with a note pointing back at the fix. That is the shape monitoring is meant to take here, periodic and no-SLA, matching how a solo-maintained hobby project is actually run: new and regressed errors trigger an email, everything else waits for a look when there's time to give it one.