Files
Sluijsens 99f74a9db9
Continuous Integration / config (pull_request) Successful in 11s
Continuous Integration / prepare (pull_request) Successful in 1m18s
Continuous Integration / build-production (pull_request) Skipped
Continuous Integration / build (pull_request) Successful in 2m2s
Continuous Integration / test (pull_request) Successful in 1m52s
Continuous Integration / deploy-production (pull_request) Skipped
Deploy / deploy (pull_request) Successful in 37s
Continuous Integration / deploy-test (pull_request) Successful in 36s
Break the world-knop alleen nog lokaal zichtbaar
De knop was ook zichtbaar op de testomgeving (VITE_APP_ENV=test).
Nu uitsluitend zichtbaar bij pnpm dev, zodat de testomgeving een
productie-equivalente build draait.
2026-08-15 20:31:06 +02:00

10 KiB

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.<region>.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