diff --git a/docs/adr/018-glitchtip-frontend-error-tracking.md b/docs/adr/018-glitchtip-frontend-error-tracking.md new file mode 100644 index 00000000..15e0ed83 --- /dev/null +++ b/docs/adr/018-glitchtip-frontend-error-tracking.md @@ -0,0 +1,86 @@ +# ADR-018: GlitchTip frontend error tracking via @sentry/sveltekit + +**Date:** 2026-05-17 +**Status:** Accepted +**Deciders:** Marcel Raddatz + +--- + +## Context + +The Familienarchiv had no client-side error reporting. When a user encountered a crash +or unhandled error in the SvelteKit frontend, there was no way for the operator to +observe it — errors were invisible until a user manually reported them. A GlitchTip +instance (self-hosted, Sentry-compatible) was already running as part of the +observability stack (`docker-compose.observability.yml`). The backend already reported +server-side errors to it. + +We needed a way to: +1. Capture frontend errors automatically and route them to GlitchTip. +2. Give users a visible error identifier they can include in a support message. +3. Do this without leaking personally identifiable information (PII) from the family + archive — documents contain personal histories, names, and relationships. + +--- + +## Decision + +Use `@sentry/sveltekit` (the official Sentry SDK for SvelteKit) to: + +- Initialise with `sendDefaultPii: false` on both `hooks.server.ts` and `hooks.client.ts`. +- Pass a callback to `Sentry.handleErrorWithSentry()` that returns + `{ message, errorId }` where `errorId` is `Sentry.lastEventId()` when Sentry + captured the event, or a fresh `crypto.randomUUID()` as fallback. +- Display the `errorId` on the `+error.svelte` page so users can include it in a + report to the operator. + +The SDK is initialised with `enabled: !!import.meta.env.VITE_SENTRY_DSN` so that +development and CI builds without a DSN configured do not send any events. + +`VITE_SENTRY_DSN` is a write-only ingest key — it can POST events to GlitchTip but +cannot read them. It is safe to include in the client bundle per the Sentry security +model; it does not require rotation like a password. + +--- + +## Alternatives considered + +**Sentry SaaS** — rejected. The archive contains private family documents and personal +history. Sending error events with stack traces to a US-hosted third party is +inconsistent with the project's data-minimisation posture. Self-hosted GlitchTip on +the same Hetzner VPS keeps all data on infrastructure the operator controls. + +**Custom error logging endpoint** — rejected. The @sentry/sveltekit SDK handles +SvelteKit's hook lifecycle, source-map upload, and event grouping automatically. +Reimplementing this would cost significant engineering time for no benefit. + +**Log-only (no user-visible errorId)** — rejected. Without a visible error ID, users +can only describe what happened in natural language, making it hard to correlate a +report with a specific GlitchTip event. The `errorId` closes this gap at negligible UI +cost. + +--- + +## Consequences + +**Positive:** +- Frontend errors are now observable without requiring user reports. +- Users can provide an `errorId` that maps directly to a GlitchTip event. +- `sendDefaultPii: false` ensures names, IPs, and cookie values are not included in + captured events. +- `tracesSampleRate: 0.1` limits trace volume to 10% of transactions, keeping + GlitchTip load low on the shared VPS. + +**Negative / trade-offs:** +- The `@sentry/sveltekit` SDK is now a production dependency. SDK updates must be + reviewed for changes to the default PII scrubbing behaviour. +- The `handleError` callback in both hooks returns a hardcoded English message + (`'An unexpected error occurred'`). This bypasses Paraglide i18n — the error page + will always show English text when the hooks are active, regardless of the user's + locale. This is acceptable because: (a) the error page is a last-resort fallback + not part of normal UX, (b) the `errorId` is the actionable information, not the + message text. A future ADR may address this if internationalised error messages + become a requirement. +- `Sentry.lastEventId()` returns `undefined` when Sentry did not capture the event + (e.g. DSN not configured). The `crypto.randomUUID()` fallback guarantees an `errorId` + is always present, but that UUID will not appear in GlitchTip.