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.
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
ErrorBoundarycomponent 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.errorinsideErrorBoundary.componentDidCatch— unchanged, zero cost, useful for local/manual debugging.
Sentry free tier (implemented)
@sentry/reactis a dependency;src/main.tsxcallsSentry.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.componentDidCatchcallsSentry.captureException(error, { extra: { componentStack: info.componentStack } })in addition toconsole.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, withtracesSampleRate: 1.0(capture all — appropriate for a low-traffic marketing site; lower this if traffic grows significantly). - Environment/release tagging:
environmentis set toimport.meta.env.VITE_APP_ENV('test'/'production', set at build time — see below), falling back toimport.meta.env.MODEfor local development (pnpm dev→'development'). This fallback is needed becausevite buildruns in production mode by default regardless of target environment, soMODEalone cannot distinguish a test build from a production build.releaseis set to the app version frompackage.json(injected at build time viavite.config.ts'sdefine: { __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 insrc/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 theBuildstep incontinuous_integration.yaml. - Local development DSN: for
pnpm dev, Vite automatically loads a.env.localfile (already covered by.gitignore's*.localrule, so it is never committed). Copy.env.exampleto.env.localand setVITE_SENTRY_DSNthere 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_DSNrepository 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.ioare 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 setstunnel: '/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.proxyinvite.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 nginxlocation /sentry-tunnelblock and the Viteserver.proxytarget must match whateverVITE_SENTRY_DSN/.env.localDSN 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.tsxrenders a "Break the world" button, mounted globally viaRootLayout.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'simport.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.*requiresenableLogs: trueinSentry.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.onerrorhandler, exactly like the official Sentry test snippet. - This is a temporary verification tool, not a permanent feature — remove
SentryTestButton(and its usage inRootLayout.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 intocontinuous_integration.yaml'sBuildstep as Gitea repository variables, not secrets (same pattern asVITE_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:
- Create a free UptimeRobot account (https://uptimerobot.com).
- Register
test.slpsoftware.nlas an HTTP(S) monitor now (checking for a200response); add the production URL once hosting is finalized (seeoperations/deployment/deployment-plan.md"Open Item") — still an open follow-up. - Optional: publish a public status page if desired for transparency to visitors.
- 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 |