# Monitoring Setup Instructions Concrete setup steps for the approaches chosen in `monitoring-plan.md`: **Logging** and **Dashboards** (no Alerting). ## Logging ### What to log - Uncaught JavaScript errors / exceptions (the app already has an `ErrorBoundary` component from Code Generation — this is the natural hook point). - Broken/failed navigation (e.g. an unexpected router error). - No user PII, form input, or sensitive data should ever be logged — this is a public marketing site, but keep this discipline regardless. ### Destination — decided: Sentry free tier + console Both the console and Sentry are now active (original Question 3 resolved as a combination): **Console (always on)** - Errors already surface via `console.error` inside `ErrorBoundary.componentDidCatch` — unchanged, zero cost, useful for local/manual debugging. **Sentry free tier (implemented)** - `@sentry/react` is a dependency; `src/main.tsx` calls `Sentry.init({ ... })` at startup, but only when a DSN is present — if not configured, Sentry is silently skipped and only console-logging remains active (safe default, no crash on missing config). - `ErrorBoundary.componentDidCatch` calls `Sentry.captureException(error, { extra: { componentStack: info.componentStack } })` in addition to `console.error`. - **Tracing/performance** is also enabled (Sentry's recommended default alongside error monitoring, per the official React SDK setup guide): `tanstackRouterBrowserTracingIntegration(router)` is wired up so route navigations are captured as transactions, with `tracesSampleRate: 1.0` (capture all — appropriate for a low-traffic marketing site; lower this if traffic grows significantly). - **Environment/release tagging**: `environment` is set to `import.meta.env.VITE_APP_ENV` (`'test'` / `'production'`, set at build time — see below), falling back to `import.meta.env.MODE` for local development (`pnpm dev` → `'development'`). This fallback is needed because `vite build` runs in production mode by default regardless of target environment, so `MODE` alone cannot distinguish a test build from a production build. `release` is set to the app version from `package.json` (injected at build time via `vite.config.ts`'s `define: { __APP_VERSION__ }`), so events in Sentry can be filtered/grouped per environment and per shipped version. - The DSN is injected at build time via Vite's `import.meta.env.VITE_SENTRY_DSN` (typed in `src/vite-env.d.ts`). It is **not** a secret (Sentry DSNs are safe to expose client-side), so it is passed as a **Gitea Actions repository variable** (`vars.VITE_SENTRY_DSN`, not a secret) to the `Build` step in `continuous_integration.yaml`. - **Local development DSN**: for `pnpm dev`, Vite automatically loads a `.env.local` file (already covered by `.gitignore`'s `*.local` rule, so it is never committed). Copy `.env.example` to `.env.local` and set `VITE_SENTRY_DSN` there to enable Sentry locally — a separate Sentry project/DSN is recommended so local test noise doesn't mix with the test-environment data. Omitting `.env.local` (or leaving the value empty) simply disables Sentry locally, falling back to console-only logging. - **Manual follow-up required**: create a free Sentry project (https://sentry.io) for this app, copy its DSN, and set it as the `VITE_SENTRY_DSN` repository variable in Gitea (Repository Settings → Actions → Variables). Until that variable is set, the build still succeeds and the site still works — Sentry reporting simply stays inactive. - Free tier limits (error volume, retention) are typically sufficient for a low-traffic marketing site. - **Not (yet) implemented, by explicit choice**: automatic source map upload (via `@sentry/vite-plugin`), which the official Sentry setup guide also recommends so stack traces show real source code instead of minified code. This requires a Sentry auth token/org/project as a new Gitea secret; deliberately left out of scope for now — revisit if readable production stack traces become a priority. - **Ad-blocker mitigation via a Sentry "tunnel" (all environments, including local)**: requests straight to `*.ingest..sentry.io` are commonly blocked client-side by ad-blockers/privacy extensions (`ERR_BLOCKED_BY_CLIENT`), because they resemble third-party tracking. To avoid this, `Sentry.init()` always sets `tunnel: '/sentry-tunnel'`. In deployed (test/production) environments, the reverse-proxy Pi (`nginx/reverse-proxy-nginx.conf.example`) forwards that path server-side to Sentry's envelope endpoint, so the browser only ever talks to the first-party domain (e.g. `test.slpsoftware.nl`). Locally (`pnpm dev`), the equivalent route is provided by Vite's own dev-server proxy (`server.proxy` in `vite.config.ts`), so no ad-blocker whitelisting/disabling is needed anymore for local testing either. This relies on full control over the reverse-proxy's nginx config, which is the case here (self-hosted on the user's own Raspberry Pi's) — it would not work on a third-party/shared host without reverse-proxy access. **Important**: the org-/project-id hardcoded in both the nginx `location /sentry-tunnel` block and the Vite `server.proxy` target must match whatever `VITE_SENTRY_DSN`/`.env.local` DSN is actually configured; update all together if the Sentry project/DSN ever changes. **Log level strategy**: only errors are logged (no verbose/info-level client logging) — this is a static site with no meaningful "business events" beyond page views, which are covered by analytics (see Dashboards below), not logging. ### Temporary manual test tool: `SentryTestButton` - `src/components/SentryTestButton.tsx` renders a "Break the world" button, mounted globally via `RootLayout.tsx`, used to manually verify that errors, logs (`Sentry.logger.info`), and metrics (`Sentry.metrics.count`) actually arrive in Sentry end-to-end. - **Visibility**: only shown during local development (`pnpm dev`, via Vite's `import.meta.env.DEV`). It is hidden in every build (test and production alike), so the test environment always runs a production-equivalent build that can be tested as-is. - **Visual feedback**: clicking the button immediately shows a green confirmation toast ("Test error verzonden naar Sentry ✅", `role="status"`) that auto-hides after 4 seconds, so the user gets clear confirmation that the test action fired — without this, the resulting uncaught error/blank state gave no indication anything happened. The actual log/metric/throw (`handleSentryTestErrorClick`) is fired on the next tick (`setTimeout(..., 0)`) so the toast has a chance to render/paint first. - `Sentry.logger.*` requires `enableLogs: true` in `Sentry.init()` (`src/main.tsx`) — added specifically to support this test button (and any future structured logging). - The thrown error is **intentionally uncaught**: React error boundaries do not catch errors thrown from event handlers (only render/lifecycle errors), so this relies on Sentry's own global `window.onerror` handler, exactly like the official Sentry test snippet. - This is a temporary verification tool, not a permanent feature — remove `SentryTestButton` (and its usage in `RootLayout.tsx`) once Sentry has been confirmed to receive test errors/logs/metrics end-to-end. ## Dashboards ### Website analytics — decided: self-hosted Umami **Decided**: self-hosted Umami, running via Podman on the webserver Pi (Pi Main, `192.168.1.103:3001` — note: not the default 3000, since Gitea already occupies that port on the same Pi), reachable for the dashboard itself via `analytics.slpsoftware.nl` (reverse-proxied + SSL via certbot, same pattern as `test.slpsoftware.nl`). Chosen over GA4 for privacy-friendliness (typically no cookie banner needed) and full self-hosted control, and over a hosted Umami/Plausible plan to avoid recurring cost. - Full step-by-step setup: `operations/monitoring/umami-setup.md` (Podman/compose files, systemd auto-start, reverse proxy + SSL, website registration). - Example config: `operations/deployment/umami/podman-compose.yml.example` + `.env.example`; `operations/deployment/nginx/analytics-nginx.conf.example`. - The tracking script is injected client-side by `src/components/UmamiAnalytics.tsx`, gated on two build-time variables (`VITE_UMAMI_SCRIPT_URL`, `VITE_UMAMI_WEBSITE_ID`), wired into `continuous_integration.yaml`'s `Build` step as Gitea repository variables, not secrets (same pattern as `VITE_SENTRY_DSN`). It never loads during local development (`pnpm dev`), even if those variables happen to be set, so local testing never pollutes visitor analytics. - Key metrics to surface: unique visitors, page views per route (Home, Packages, etc. — see `frontend-components.md`), and referral sources. ### Uptime dashboard — decided: UptimeRobot **Decided**: UptimeRobot (free tier: up to 50 monitors, 5-minute check interval, optional e-mail notification on downtime — opportunistic, not a designed alerting feature per `monitoring-plan.md`). Chosen over Better Uptime for its long-standing free tier and simplicity for a single low-traffic site. **Setup**: 1. Create a free UptimeRobot account (https://uptimerobot.com). 2. Register `test.slpsoftware.nl` as an HTTP(S) monitor now (checking for a `200` response); add the production URL once hosting is finalized (see `operations/deployment/deployment-plan.md` "Open Item") — still an open follow-up. 3. Optional: publish a public status page if desired for transparency to visitors. 4. Key metric to surface: uptime percentage / current status. No application code changes are needed for UptimeRobot — it works purely by polling the public URL from the outside, entirely independent of the site's own codebase. ## Summary Table | Concern | Approach | Status | |---|---|---| | Client-side errors | Logging (console + Sentry free tier, incl. tracing, environment/release tags, and an ad-blocker-proof tunnel) | Implemented and confirmed working end-to-end (errors, logs, and metrics received) locally and on the test environment; the `VITE_SENTRY_DSN` Gitea repository variable still needs to be created by the user for the test/production build to report to Sentry (local already works via `.env.local`) | | Visitor/usage insight | Analytics dashboard (self-hosted Umami) | Tool decided; deployment (Podman on Pi Main + `analytics.slpsoftware.nl`) is a manual follow-up, see `umami-setup.md` | | Site reachability | Uptime dashboard (UptimeRobot) | Tool decided; account creation + production URL are manual follow-ups | | Alerting | Out of scope | Not configured | | Shared infrastructure reuse | Out of scope | None exists yet |