Feature/analytics uptime dashboard #6
@@ -8,3 +8,10 @@
|
|||||||
# Sentry-project aan op https://sentry.io en plak hier de DSN van dat project.
|
# Sentry-project aan op https://sentry.io en plak hier de DSN van dat project.
|
||||||
# Laat leeg/weg om Sentry lokaal uit te schakelen (alleen console-logging blijft actief).
|
# Laat leeg/weg om Sentry lokaal uit te schakelen (alleen console-logging blijft actief).
|
||||||
VITE_SENTRY_DSN=
|
VITE_SENTRY_DSN=
|
||||||
|
|
||||||
|
# Self-hosted Umami analytics (zie operations/monitoring/umami-setup.md).
|
||||||
|
# Geen van beide is een secret (client-side zichtbaar in de pagina-broncode).
|
||||||
|
# Laat leeg/weg om analytics lokaal uit te schakelen; in `pnpm dev` wordt het
|
||||||
|
# script sowieso nooit geladen, ongeacht deze waarden (zie UmamiAnalytics.tsx).
|
||||||
|
VITE_UMAMI_SCRIPT_URL=https://analytics.slpsoftware.nl/script.js
|
||||||
|
VITE_UMAMI_WEBSITE_ID=
|
||||||
|
|||||||
@@ -92,8 +92,9 @@ jobs:
|
|||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm run build
|
|
||||||
env:
|
env:
|
||||||
|
VITE_UMAMI_SCRIPT_URL: ${{ vars.VITE_UMAMI_SCRIPT_URL }}
|
||||||
|
VITE_UMAMI_WEBSITE_ID: ${{ vars.VITE_UMAMI_WEBSITE_ID }}
|
||||||
# Sentry DSN is niet gevoelig (veilig om in de client-bundle te zitten),
|
# Sentry DSN is niet gevoelig (veilig om in de client-bundle te zitten),
|
||||||
# daarom een Gitea Actions "vars"-waarde i.p.v. een secret. Optioneel:
|
# daarom een Gitea Actions "vars"-waarde i.p.v. een secret. Optioneel:
|
||||||
# als deze niet is ingesteld, wordt Sentry-logging simpelweg overgeslagen
|
# als deze niet is ingesteld, wordt Sentry-logging simpelweg overgeslagen
|
||||||
@@ -104,6 +105,7 @@ jobs:
|
|||||||
# Zolang er nog geen aparte productie-build/deploy bestaat, is dit altijd
|
# Zolang er nog geen aparte productie-build/deploy bestaat, is dit altijd
|
||||||
# gelijk aan DEPLOY_ENVIRONMENT ('test').
|
# gelijk aan DEPLOY_ENVIRONMENT ('test').
|
||||||
VITE_APP_ENV: ${{ env.DEPLOY_ENVIRONMENT }}
|
VITE_APP_ENV: ${{ env.DEPLOY_ENVIRONMENT }}
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
- **Project Type**: Greenfield
|
- **Project Type**: Greenfield
|
||||||
- **Start Date**: 2026-07-18T00:37:00Z
|
- **Start Date**: 2026-07-18T00:37:00Z
|
||||||
- **Current Stage**: Complete (post-completion follow-ups tracked in `audit.md` / `operations/production-readiness-checklist.md`)
|
- **Current Stage**: Complete (post-completion follow-ups tracked in `audit.md` / `operations/production-readiness-checklist.md`)
|
||||||
- **Branch**: feature/sentry_error_logging
|
- **Branch**: feature/analytics_uptime_dashboard
|
||||||
|
|
||||||
## Workspace State
|
## Workspace State
|
||||||
- **Existing Code**: No
|
- **Existing Code**: No
|
||||||
|
|||||||
@@ -564,3 +564,75 @@
|
|||||||
**Context**: Closes out the Sentry logging-destination follow-up. Three open follow-ups remain per `production-readiness-checklist.md`: production hosting/deploy, analytics/uptime dashboard tool selection, and a formal Lighthouse performance check.
|
**Context**: Closes out the Sentry logging-destination follow-up. Three open follow-ups remain per `production-readiness-checklist.md`: production hosting/deploy, analytics/uptime dashboard tool selection, and a formal Lighthouse performance check.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Post-Completion Follow-up: Analytics + Uptime Dashboard Tools Decided (Umami self-hosted + UptimeRobot)
|
||||||
|
|
||||||
|
**Timestamp**: 2026-07-25T16:10:00Z
|
||||||
|
|
||||||
|
**User Input**: "Analytics/uptime dashboard" open item chosen next, after closing the Sentry logging follow-up on its own branch. Clarified: Umami (self-hosted, needs Podman help), UptimeRobot, hosted on the webserver Pi (Pi Main), dashboard reachable via `analytics.slpsoftware.nl`, Podman already available on that Pi.
|
||||||
|
|
||||||
|
**AI Response**: "Created feature branch `feature/analytics_uptime_dashboard` from `master` (independent of the not-yet-merged `feature/sentry_error_logging` branch). Added `src/components/UmamiAnalytics.tsx`, which injects the Umami tracking script client-side, gated on `VITE_UMAMI_SCRIPT_URL`/`VITE_UMAMI_WEBSITE_ID` (typed in a new `src/vite-env.d.ts`) and skipped entirely during local development, mirroring the existing Sentry-DSN pattern; mounted it in `RootLayout.tsx`, with unit tests (`UmamiAnalytics.test.tsx`, 4 tests). Wired both variables into `continuous_integration.yaml`'s `Build` step as Gitea repository variables (not secrets). Added a self-hosted Umami example setup: `operations/deployment/umami/podman-compose.yml.example` + `.env.example` (Umami + Postgres, bound to the Pi's internal IP only) and `operations/deployment/nginx/analytics-nginx.conf.example` (reverse-proxy Pi forwarding `analytics.slpsoftware.nl` to Pi Main's Umami port, with certbot SSL, same pattern as the test-environment config). Wrote a new step-by-step `operations/monitoring/umami-setup.md` covering container setup, systemd auto-start (rootless Podman + `loginctl enable-linger`), reverse proxy/SSL, website registration in the Umami UI, and wiring the resulting website ID into the build. Updated `monitoring-plan.md`, `monitoring-setup.md`, and `production-readiness-checklist.md` to mark the analytics/uptime tool-selection open item as resolved, listing the remaining manual deployment steps."
|
||||||
|
|
||||||
|
**Context**: Post-workflow follow-up (feature already marked Complete); addresses the "Analytics/uptime dashboard" open item from `production-readiness-checklist.md`. Work done on `feature/analytics_uptime_dashboard`. `pnpm run build`, `pnpm run lint`, and `pnpm run test` to be verified before committing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Completion Follow-up: Umami Moved to a Dedicated `umami` User (Podman Isolation Pattern)
|
||||||
|
|
||||||
|
**Timestamp**: 2026-07-25T21:20:00Z
|
||||||
|
|
||||||
|
**User Input**: "Ja, ik wil wel meer de kant op gaan van de verschillende self-hosted services als losse users. Maakt mij flexibeler als ik later meer wil ookal is een raspberry pi beperkt in rekenkracht" (confirming, after a prior explanation, the preference for a dedicated Linux user per self-hosted service instead of a shared folder under the existing user account).
|
||||||
|
|
||||||
|
**AI Response**: "Reworked `operations/monitoring/umami-setup.md` to run Umami under a new dedicated `umami` Linux user instead of the existing main user account: added a new "Waarom een dedicated user" section explaining the rootless-Podman isolation rationale, and a new step 0 covering `useradd --create-home`, verifying/adding a subuid/subgid range (`/etc/subuid`/`/etc/subgid`), and `loginctl enable-linger umami` so rootless Podman services keep running without an active login. Updated step 1 (container setup) and step 2 (systemd auto-start) to run as the `umami` user (`sudo -iu umami`), with paths now under `/home/umami/umami/`. Extended the outstanding manual steps list with the new user-creation step, and added a closing note documenting this as a reusable pattern for future self-hosted services on the same or other Pi's."
|
||||||
|
|
||||||
|
**Context**: Post-workflow follow-up (feature already marked Complete); continues the `feature/analytics_uptime_dashboard` branch, refining the Umami deployment approach before it has been executed on the Pi. Documentation-only change (no code/build impact).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Completion Follow-up: Umami External Port Changed (3000 → 3001) to Avoid Conflict with Gitea
|
||||||
|
|
||||||
|
**Timestamp**: 2026-07-25T22:39:00Z
|
||||||
|
|
||||||
|
**User Input**: "de poort 3000 is la in gebruik. volgens mij door gitea: Error: unable to start container ...: rootlessport listen tcp 192.168.1.103:3000: bind: address already in use"
|
||||||
|
|
||||||
|
**AI Response**: "Confirmed the reported cause: Gitea already occupies port 3000 system-wide on the same Pi, so even though Umami's `podman-compose.yml.example` binds specifically to `192.168.1.103:3000` (not `0.0.0.0`), it still conflicts because Gitea's own binding already covers that IP/port combination. Changed the external port mapping to `192.168.1.103:3001:3000` (container still listens internally on 3000), and updated all references accordingly: `nginx/analytics-nginx.conf.example` (`proxy_pass` target + comments), `monitoring-setup.md` (decision summary), and `umami-setup.md` (heartbeat-check `curl` command, with an explanatory note)."
|
||||||
|
|
||||||
|
**Context**: Post-workflow follow-up (feature already marked Complete); continues the `feature/analytics_uptime_dashboard` branch, fixing a port conflict discovered before the Umami deployment has been executed on the Pi. Documentation/example-config-only change (no code/build impact).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Completion Follow-up: Clarified `podman-compose down`/`up -d` Needed After Config Changes to an Already-Created Container
|
||||||
|
|
||||||
|
**Timestamp**: 2026-07-25T22:57:00Z
|
||||||
|
|
||||||
|
**User Input**: "maakt het uit dat ik al podman-compose heb uitgevoerd? moet ik nu eerst de container verwijderen?" (asking whether it matters that `podman-compose up -d` had already been run with the old port-3000 config, before the port-3001 fix).
|
||||||
|
|
||||||
|
**AI Response**: "Explained that a port mapping is set at container-creation time and is not live-reloaded by `podman-compose up -d` on an already-existing container, so after updating the local `podman-compose.yml` to the new port, a `podman-compose down` followed by `podman-compose up -d` is required to recreate the container with the corrected mapping (database data is preserved via the persistent volume). Added a callout to `umami-setup.md` documenting this `down`/`up` requirement for future config changes (e.g. another port change), so this is not only explained in chat but also captured for later reference."
|
||||||
|
|
||||||
|
**Context**: Post-workflow follow-up (feature already marked Complete); continues the `feature/analytics_uptime_dashboard` branch, clarifying the deployment steps before the Umami container has successfully started on the Pi. Documentation-only change (no code/build impact).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Completion Follow-up: ufw Firewall Rule Missing for Port 3001 (Connection Reset by Peer)
|
||||||
|
|
||||||
|
**Timestamp**: 2026-07-25T23:05:00Z
|
||||||
|
|
||||||
|
**User Input**: "ik krijg nu: curl http://192.168.1.103:3001/api/heartbeat curl: (56) Recv failure: Connection reset by peer"
|
||||||
|
|
||||||
|
**AI Response**: "Diagnosed via `podman ps`/`podman logs`/`ss -tlnp` that the Umami container and rootlessport forwarder were both healthy and correctly listening on `192.168.1.103:3001`, ruling out an application/container issue. Root cause found in `sudo ufw status verbose`: an explicit `ALLOW` rule exists for `3000/tcp` from `192.168.1.102` (the reverse-proxy Pi), but no equivalent rule exists for `3001/tcp`, and ufw's default incoming policy is `deny` — so the request is blocked even when curled from Pi Main itself, since it targets the real host IP rather than loopback. Added a new firewall step to `umami-setup.md` (`sudo ufw allow from 192.168.1.102 to any port 3001 proto tcp` + `ufw reload`), inserted before the heartbeat-check step, plus a troubleshooting tip to test via `localhost:3001` (bypasses the firewall) to isolate firewall vs. container issues. Added the missing ufw rule to the outstanding manual steps list."
|
||||||
|
|
||||||
|
**Context**: Post-workflow follow-up (feature already marked Complete); continues the `feature/analytics_uptime_dashboard` branch, resolving a connectivity blocker discovered while the user was testing the Umami container on Pi Main. Documentation-only change (no code/build impact); the actual `ufw allow` command still needs to be run by the user on Pi Main.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Completion Follow-up: "Failed to connect to bus: No medium found" When Setting Up the systemd User Service
|
||||||
|
|
||||||
|
**Timestamp**: 2026-07-25T23:18:00Z
|
||||||
|
|
||||||
|
**User Input**: "ik krijg het volgende al sik de service probeer aan te maken en deamon reload probeer te doen: systemctl --user daemon-reload Failed to connect to bus: No medium found"
|
||||||
|
|
||||||
|
**AI Response**: "Explained that this error means no D-Bus/systemd user session exists yet for the `umami` user, most likely because `sudo -iu umami` does not register a full PAM login session the way an interactive login does — so `/run/user/<uid>` (and the bus socket inside it) is missing, even with linger already enabled. Added a troubleshooting callout to `umami-setup.md` right before the systemd-service step: verify `Linger=yes` via `loginctl show-user umami`, re-run `loginctl enable-linger umami` if not, explicitly start `user@<uid>.service`, re-login and export `XDG_RUNTIME_DIR` manually, with `machinectl shell umami@ /bin/bash` offered as a more robust alternative to `sudo -iu`/`su -` if the issue persists."
|
||||||
|
|
||||||
|
**Context**: Post-workflow follow-up (feature already marked Complete); continues the `feature/analytics_uptime_dashboard` branch, resolving a blocker discovered while the user was setting up the systemd auto-start service on Pi Main. Documentation-only change (no code/build impact); the user still needs to apply the fix on Pi Main.
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ Deze secrets heten `PI_MAIN_*` (niet `PI_TEST_*`), omdat dezelfde Pi (Pi Main) e
|
|||||||
|
|
||||||
## Eenmalige Setup — nginx & SSL op de Raspberry Pi's
|
## Eenmalige Setup — nginx & SSL op de Raspberry Pi's
|
||||||
1. Kopieer `operations/deployment/nginx/webserver-nginx.conf.example` naar `/etc/nginx/sites-available/` op de webserver-Pi, maak een symlink in `sites-enabled/`, en herlaad nginx. Dit bestand is de daadwerkelijk in gebruik zijnde configuratie (`server_name test.slpsoftware.nl`, luistert op poort 80, serveert vanaf `/mnt/storage1/www/html/test/slpsoftware`).
|
1. Kopieer `operations/deployment/nginx/webserver-nginx.conf.example` naar `/etc/nginx/sites-available/` op de webserver-Pi, maak een symlink in `sites-enabled/`, en herlaad nginx. Dit bestand is de daadwerkelijk in gebruik zijnde configuratie (`server_name test.slpsoftware.nl`, luistert op poort 80, serveert vanaf `/mnt/storage1/www/html/test/slpsoftware`).
|
||||||
2. Kopieer `operations/deployment/nginx/reverse-proxy-nginx.conf.example` naar `/etc/nginx/sites-available/slpsoftware-test.conf` op de reverse-proxy-Pi, maak een symlink in `sites-enabled/`, en herlaad nginx. Ook dit bestand is de daadwerkelijk in gebruik zijnde (door certbot beheerde) configuratie, met `server_name test.slpsoftware.nl`.
|
2. Kopieer `operations/deployment/nginx/reverse-proxy-nginx.conf.example` naar `/etc/nginx/sites-available/slpsoftware-test.conf` op de reverse-proxy-Pi, maak een symlink in `sites-enabled/`, en herlaad nginx. Dit is de versie van vóór certbot (alleen poort 80, geen SSL), met `server_name test.slpsoftware.nl`.
|
||||||
3. Vraag op de reverse-proxy-Pi een SSL-certificaat aan met certbot (Let's Encrypt), nadat het DNS-record klopt: `sudo certbot --nginx -d test.slpsoftware.nl`. Certbot regelt automatisch de HTTPS-configuratie en de HTTP→HTTPS-redirect (net zoals je gewend bent van certbot).
|
3. Vraag op de reverse-proxy-Pi een SSL-certificaat aan met certbot (Let's Encrypt), nadat het DNS-record klopt: `sudo certbot --nginx -d test.slpsoftware.nl`. Certbot herschrijft dit bestand automatisch met de HTTPS-configuratie en de HTTP→HTTPS-redirect — zie `operations/deployment/nginx/reverse-proxy-nginx.conf.post-certbot.example` voor hoe het er dan uitziet (referentie, niet zelf kopiëren).
|
||||||
4. Zorg dat de map `/mnt/storage1/www/html/test/slpsoftware` bestaat op de webserver-Pi en schrijfbaar is voor de gebruiker `webadmin` (bijv. `sudo mkdir -p /mnt/storage1/www/html/test/slpsoftware && sudo chown webadmin:webadmin /mnt/storage1/www/html/test/slpsoftware`).
|
4. Zorg dat de map `/mnt/storage1/www/html/test/slpsoftware` bestaat op de webserver-Pi en schrijfbaar is voor de gebruiker `webadmin` (bijv. `sudo mkdir -p /mnt/storage1/www/html/test/slpsoftware && sudo chown webadmin:webadmin /mnt/storage1/www/html/test/slpsoftware`).
|
||||||
|
|
||||||
> **Waarom `deploy_path` en de nginx `root` niet hetzelfde pad zijn**: de pipeline uploadt via SCP naar `deploy_path` = `/html/test/slpsoftware` (zie `env.DEPLOY_PATH` in `continuous_integration.yaml`), terwijl de nginx `root` in `webserver-nginx.conf.example` het volledige pad `/mnt/storage1/www/html/test/slpsoftware` is. Dit is geen fout of inconsistentie: de SSH/SCP-gebruiker (`webadmin`) heeft `/mnt/storage1/www` als root (vergelijkbaar met een FTP-chroot), dus vanuit het perspectief van deze gebruiker is `/html/test/slpsoftware` het juiste (relatieve) pad, terwijl dat op het bestandssysteem van de Pi zelf overeenkomt met het volledige pad `/mnt/storage1/www/html/test/slpsoftware` dat nginx als `root` gebruikt. Kortom: `deploy_path` (`/html/test/slpsoftware`) + de root van de `webadmin`-gebruiker (`/mnt/storage1/www`) = de nginx `root` (`/mnt/storage1/www/html/test/slpsoftware`).
|
> **Waarom `deploy_path` en de nginx `root` niet hetzelfde pad zijn**: de pipeline uploadt via SCP naar `deploy_path` = `/html/test/slpsoftware` (zie `env.DEPLOY_PATH` in `continuous_integration.yaml`), terwijl de nginx `root` in `webserver-nginx.conf.example` het volledige pad `/mnt/storage1/www/html/test/slpsoftware` is. Dit is geen fout of inconsistentie: de SSH/SCP-gebruiker (`webadmin`) heeft `/mnt/storage1/www` als root (vergelijkbaar met een FTP-chroot), dus vanuit het perspectief van deze gebruiker is `/html/test/slpsoftware` het juiste (relatieve) pad, terwijl dat op het bestandssysteem van de Pi zelf overeenkomt met het volledige pad `/mnt/storage1/www/html/test/slpsoftware` dat nginx als `root` gebruikt. Kortom: `deploy_path` (`/html/test/slpsoftware`) + de root van de `webadmin`-gebruiker (`/mnt/storage1/www`) = de nginx `root` (`/mnt/storage1/www/html/test/slpsoftware`).
|
||||||
|
|||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
# Voorbeeldconfiguratie voor de nginx reverse proxy op de reverse-proxy-Pi
|
||||||
|
# (dezelfde Pi als reverse-proxy-nginx.conf.example) om het zelf-gehoste
|
||||||
|
# Umami-analyticsdashboard bereikbaar te maken via analytics.slpsoftware.nl.
|
||||||
|
#
|
||||||
|
# Umami zelf draait als podman-container op de webserver-Pi (Pi Main,
|
||||||
|
# 192.168.1.103:3001 — zie ../umami/podman-compose.yml.example) en heeft dus
|
||||||
|
# GEEN eigen nginx-configuratie op die Pi nodig; Umami serveert zijn eigen
|
||||||
|
# dashboard + tracking-script rechtstreeks via die poort. Let op: de externe
|
||||||
|
# poort is bewust 3001 (niet het gebruikelijke 3000), omdat Gitea op diezelfde
|
||||||
|
# Pi al poort 3000 systeembreed bezet.
|
||||||
|
#
|
||||||
|
# Dit is de versie die je gebruikt VOORDAT certbot gedraaid heeft: alleen
|
||||||
|
# poort 80, geen SSL. Certbot heeft dit HTTP-server-block namelijk nodig om
|
||||||
|
# de ACME-challenge te kunnen afhandelen en zal, zodra je hem draait, dit
|
||||||
|
# bestand zelf herschrijven om er de HTTPS-configuratie en de HTTP→HTTPS-
|
||||||
|
# redirect aan toe te voegen. Zie analytics-nginx.conf.post-certbot.example
|
||||||
|
# voor hoe het bestand er na die stap uit gaat zien (puur ter referentie —
|
||||||
|
# dat bestand hoef je niet zelf te kopiëren, certbot genereert het).
|
||||||
|
#
|
||||||
|
# Kopieer dit bestand handmatig naar bijvoorbeeld
|
||||||
|
# /etc/nginx/sites-available/slpsoftware-analytics.conf op de reverse-proxy-Pi,
|
||||||
|
# maak een symlink in sites-enabled, herlaad nginx, en draai dan pas certbot:
|
||||||
|
# sudo certbot --nginx -d analytics.slpsoftware.nl
|
||||||
|
# Zorg dat het DNS-record voor analytics.slpsoftware.nl al naar het publieke
|
||||||
|
# IP van deze Pi wijst voordat je certbot draait.
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
|
||||||
|
server_name analytics.slpsoftware.nl;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/slpsoftware-analytics_error.log;
|
||||||
|
access_log /var/log/nginx/slpsoftware-analytics_access.log;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://192.168.1.103:3001;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
# REFERENTIE ALLEEN — dit bestand hoef je niet handmatig te kopiëren.
|
||||||
|
#
|
||||||
|
# Dit toont hoe /etc/nginx/sites-available/slpsoftware-analytics.conf op de
|
||||||
|
# reverse-proxy-Pi er automatisch uit komt te zien NADAT je certbot hebt
|
||||||
|
# gedraaid (`sudo certbot --nginx -d analytics.slpsoftware.nl`) op basis van
|
||||||
|
# analytics-nginx.conf.example. Certbot voegt zelf de HTTPS-configuratie en
|
||||||
|
# het HTTP→HTTPS-redirect-blok toe (herkenbaar aan de "managed by Certbot"
|
||||||
|
# commentaren), en zet de error_log/access_log en het proxy_pass-blok gewoon
|
||||||
|
# over naar het nieuwe HTTPS-serverblok.
|
||||||
|
server {
|
||||||
|
|
||||||
|
server_name analytics.slpsoftware.nl;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/slpsoftware-analytics_error.log;
|
||||||
|
access_log /var/log/nginx/slpsoftware-analytics_access.log;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://192.168.1.103:3001;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
listen 443 ssl; # managed by Certbot
|
||||||
|
listen [::]:443 ssl; # managed by Certbot
|
||||||
|
ssl_certificate /etc/letsencrypt/live/analytics.slpsoftware.nl/fullchain.pem; # managed by Certbot
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/analytics.slpsoftware.nl/privkey.pem; # managed by Certbot
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
if ($host = analytics.slpsoftware.nl) {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
} # managed by Certbot
|
||||||
|
|
||||||
|
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
|
||||||
|
server_name analytics.slpsoftware.nl;
|
||||||
|
return 404; # managed by Certbot
|
||||||
|
}
|
||||||
+12
-31
@@ -3,21 +3,21 @@
|
|||||||
# doorstuurt naar de webserver-Pi op 192.168.1.103:80) — voor de TESTOMGEVING,
|
# doorstuurt naar de webserver-Pi op 192.168.1.103:80) — voor de TESTOMGEVING,
|
||||||
# bereikbaar via het domein test.slpsoftware.nl.
|
# bereikbaar via het domein test.slpsoftware.nl.
|
||||||
#
|
#
|
||||||
# Kopieer dit bestand handmatig naar bijvoorbeeld
|
# Dit is de versie die je gebruikt VOORDAT certbot gedraaid heeft: alleen
|
||||||
# /etc/nginx/sites-available/slpsoftware-test.conf op de reverse-proxy-Pi,
|
# poort 80, geen SSL. Certbot heeft dit HTTP-server-block namelijk nodig om
|
||||||
# maak een symlink in sites-enabled, en herlaad nginx.
|
# de ACME-challenge te kunnen afhandelen en zal, zodra je hem draait, dit
|
||||||
|
# bestand zelf herschrijven om er de HTTPS-configuratie en de HTTP→HTTPS-
|
||||||
|
# redirect aan toe te voegen. Zie reverse-proxy-nginx.conf.post-certbot.example
|
||||||
|
# voor hoe het bestand er na die stap uit gaat zien (puur ter referentie —
|
||||||
|
# dat bestand hoef je niet zelf te kopiëren, certbot genereert het).
|
||||||
#
|
#
|
||||||
# SSL-certificaat: net als voor de productiesite wordt dit aangevraagd met
|
# Kopieer dit bestand handmatig naar
|
||||||
# certbot (Let's Encrypt) rechtstreeks op deze reverse-proxy-Pi, bijvoorbeeld:
|
# /etc/nginx/sites-available/slpsoftware-test.conf op de reverse-proxy-Pi,
|
||||||
|
# maak een symlink in sites-enabled, herlaad nginx, en draai dan pas certbot:
|
||||||
# sudo certbot --nginx -d test.slpsoftware.nl
|
# sudo certbot --nginx -d test.slpsoftware.nl
|
||||||
# Zorg dat het DNS-record voor test.slpsoftware.nl al naar het publieke IP van
|
# Zorg dat het DNS-record voor test.slpsoftware.nl al naar het publieke IP van
|
||||||
# deze Pi wijst voordat je certbot draait.
|
# deze Pi wijst voordat je certbot draait.
|
||||||
#
|
#
|
||||||
# Dit is de daadwerkelijk in gebruik zijnde configuratie op de reverse-proxy-Pi,
|
|
||||||
# zoals door certbot gegenereerd/beheerd. Het HTTP-server-block onderaan (dat
|
|
||||||
# doorverwijst naar HTTPS, behalve voor de ACME-challenge) en de #-commentaren
|
|
||||||
# "managed by Certbot" zijn automatisch door certbot toegevoegd/aangepast.
|
|
||||||
#
|
|
||||||
# LET OP - gevonden foutje: de header hieronder heet `X-Real-IP` (met
|
# LET OP - gevonden foutje: de header hieronder heet `X-Real-IP` (met
|
||||||
# koppelteken), niet `X-Real_IP` (met underscore). Een header met een
|
# koppelteken), niet `X-Real_IP` (met underscore). Een header met een
|
||||||
# underscore is geen geldige/herkenbare naam voor de standaard `$http_x_real_ip`-
|
# underscore is geen geldige/herkenbare naam voor de standaard `$http_x_real_ip`-
|
||||||
@@ -25,6 +25,8 @@
|
|||||||
# de ontvangende applicatie/module verwacht het koppelteken-formaat. Corrigeer
|
# de ontvangende applicatie/module verwacht het koppelteken-formaat. Corrigeer
|
||||||
# dit op de Pi zelf naar `X-Real-IP` zodra je de config daar aanpast.
|
# dit op de Pi zelf naar `X-Real-IP` zodra je de config daar aanpast.
|
||||||
server {
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
|
||||||
server_name test.slpsoftware.nl;
|
server_name test.slpsoftware.nl;
|
||||||
|
|
||||||
@@ -60,25 +62,4 @@ server {
|
|||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
listen 443 ssl; # managed by Certbot
|
|
||||||
listen [::]:443 ssl; # managed by Certbot
|
|
||||||
ssl_certificate /etc/letsencrypt/live/test.slpsoftware.nl/fullchain.pem; # managed by Certbot
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/test.slpsoftware.nl/privkey.pem; # managed by Certbot
|
|
||||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
|
||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
|
||||||
if ($host = test.slpsoftware.nl) {
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
} # managed by Certbot
|
|
||||||
|
|
||||||
|
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
|
|
||||||
server_name test.slpsoftware.nl;
|
|
||||||
return 404; # managed by Certbot
|
|
||||||
}
|
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
# REFERENTIE ALLEEN — dit bestand hoef je niet handmatig te kopiëren.
|
||||||
|
#
|
||||||
|
# Dit toont hoe /etc/nginx/sites-available/slpsoftware-test.conf op de
|
||||||
|
# reverse-proxy-Pi er automatisch uit komt te zien NADAT je certbot hebt
|
||||||
|
# gedraaid (`sudo certbot --nginx -d test.slpsoftware.nl`) op basis van
|
||||||
|
# reverse-proxy-nginx.conf.example. Certbot voegt zelf de HTTPS-configuratie
|
||||||
|
# en het HTTP→HTTPS-redirect-blok toe (herkenbaar aan de "managed by Certbot"
|
||||||
|
# commentaren), en zet de error_log/access_log en overige location-blocks
|
||||||
|
# gewoon over naar het nieuwe HTTPS-serverblok.
|
||||||
|
server {
|
||||||
|
|
||||||
|
server_name test.slpsoftware.nl;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/slpsoftware-test_error.log;
|
||||||
|
access_log /var/log/nginx/slpsoftware-test_access.log;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /sentry-tunnel {
|
||||||
|
proxy_pass https://o4511795618185216.ingest.de.sentry.io/api/4511795622838352/envelope/;
|
||||||
|
proxy_set_header Host o4511795618185216.ingest.de.sentry.io;
|
||||||
|
proxy_ssl_server_name on;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://192.168.1.103:80;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
listen 443 ssl; # managed by Certbot
|
||||||
|
listen [::]:443 ssl; # managed by Certbot
|
||||||
|
ssl_certificate /etc/letsencrypt/live/test.slpsoftware.nl/fullchain.pem; # managed by Certbot
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/test.slpsoftware.nl/privkey.pem; # managed by Certbot
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
if ($host = test.slpsoftware.nl) {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
} # managed by Certbot
|
||||||
|
|
||||||
|
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
|
||||||
|
server_name test.slpsoftware.nl;
|
||||||
|
return 404; # managed by Certbot
|
||||||
|
}
|
||||||
+3
@@ -16,6 +16,9 @@ server {
|
|||||||
|
|
||||||
server_name test.slpsoftware.nl;
|
server_name test.slpsoftware.nl;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/slpsoftware-test-webserver_error.log;
|
||||||
|
access_log /var/log/nginx/slpsoftware-test-webserver_access.log;
|
||||||
|
|
||||||
root /mnt/storage1/www/html/test/slpsoftware;
|
root /mnt/storage1/www/html/test/slpsoftware;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Kopieer dit bestand naar `.env` naast podman-compose.yml op de Pi (NIET committen —
|
||||||
|
# bevat straks echte wachtwoorden/secrets).
|
||||||
|
|
||||||
|
# Willekeurig, sterk wachtwoord voor de Umami PostgreSQL-database.
|
||||||
|
# Genereren met bv.: openssl rand -base64 32
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
|
||||||
|
# Willekeurige, sterke random string die Umami gebruikt om sessies te ondertekenen.
|
||||||
|
# Genereren met bv.: openssl rand -base64 32
|
||||||
|
APP_SECRET=
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
# Voorbeeld podman-compose bestand om Umami (self-hosted analytics) te draaien
|
||||||
|
# op de webserver-Pi (Pi Main, 192.168.1.103), naast de al draaiende nginx die
|
||||||
|
# de website zelf serveert.
|
||||||
|
#
|
||||||
|
# Vereist: podman + podman-compose (`pip3 install --user podman-compose`) op de Pi.
|
||||||
|
#
|
||||||
|
# Gebruik:
|
||||||
|
# 1. Kopieer dit bestand naar bv. ~/umami/podman-compose.yml op de Pi.
|
||||||
|
# 2. Kopieer .env.example naast dit bestand naar .env en vul een echte
|
||||||
|
# POSTGRES_PASSWORD en APP_SECRET in (zie .env.example voor hoe je die genereert).
|
||||||
|
# 3. cd ~/umami && podman-compose up -d
|
||||||
|
# 4. Zie umami-setup.md voor het instellen van auto-start na reboot (systemd)
|
||||||
|
# en het aanmaken van een website in de Umami-UI om het website-ID te krijgen.
|
||||||
|
|
||||||
|
services:
|
||||||
|
umami:
|
||||||
|
image: docker.umami.is/umami-software/umami:postgresql-latest
|
||||||
|
# Alleen op het interne (LAN) IP van deze Pi binden, niet op 0.0.0.0 — zelfde
|
||||||
|
# aanpak als webserver-nginx.conf.example, zodat deze poort niet per ongeluk
|
||||||
|
# vanaf buiten het netwerk bereikbaar is. Externe poort is 3001 (i.p.v. het
|
||||||
|
# gebruikelijke 3000), omdat Gitea op dezelfde Pi al poort 3000 systeembreed
|
||||||
|
# bezet (ook op dit interne IP, ongeacht dat Umami een specifiek IP opgeeft).
|
||||||
|
# De container zelf luistert intern nog gewoon op 3000. De reverse-proxy-Pi
|
||||||
|
# stuurt analytics.slpsoftware.nl door naar 192.168.1.103:3001 (zie
|
||||||
|
# ../nginx/analytics-nginx.conf.example).
|
||||||
|
ports:
|
||||||
|
- "192.168.1.103:3001:3000"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://umami:${POSTGRES_PASSWORD}@db:5432/umami
|
||||||
|
DATABASE_TYPE: postgresql
|
||||||
|
APP_SECRET: ${APP_SECRET}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/heartbeat"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: docker.io/postgres:15-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: umami
|
||||||
|
POSTGRES_USER: umami
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- umami-db-data:/var/lib/postgresql/data
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U umami -d umami"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
umami-db-data:
|
||||||
@@ -17,8 +17,8 @@ Client-side errors (JavaScript crashes, broken links) are logged. **Decided**: S
|
|||||||
|
|
||||||
### Dashboards
|
### Dashboards
|
||||||
A combination of:
|
A combination of:
|
||||||
- **Website analytics** (visitors, page views, basic engagement) — e.g. a simple/free tool such as Plausible, Umami, or Google Analytics/Search Console.
|
- **Website analytics**: **decided** — self-hosted Umami (Podman on the webserver Pi, see `operations/monitoring/umami-setup.md`), chosen over GA4/hosted Plausible for privacy-friendliness and no recurring cost.
|
||||||
- **Uptime dashboard** (site reachability) — e.g. an external monitoring service such as UptimeRobot or Better Uptime.
|
- **Uptime dashboard**: **decided** — UptimeRobot, chosen over Better Uptime for its established free tier.
|
||||||
|
|
||||||
(Original Question 6 = C, "Both (analytics + uptime dashboard)".)
|
(Original Question 6 = C, "Both (analytics + uptime dashboard)".)
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ A combination of:
|
|||||||
|
|
||||||
## Open Action Items
|
## Open Action Items
|
||||||
1. ~~Decide logging destination~~ — **Resolved and verified**: Sentry free tier, wired into `ErrorBoundary`/`main.tsx` (see `monitoring-setup.md`), including tracing, environment/release tags, and a tunnel to bypass ad-blocker blocking. The user confirmed errors, logs, and metrics are received correctly, both locally and on the test environment. The Gitea Actions variable `VITE_SENTRY_DSN` still needs to be created by the user with a real Sentry project DSN for the test/production build; until then that build simply skips Sentry initialization and only console-logging is active (local development already has its own DSN via `.env.local`).
|
1. ~~Decide logging destination~~ — **Resolved and verified**: Sentry free tier, wired into `ErrorBoundary`/`main.tsx` (see `monitoring-setup.md`), including tracing, environment/release tags, and a tunnel to bypass ad-blocker blocking. The user confirmed errors, logs, and metrics are received correctly, both locally and on the test environment. The Gitea Actions variable `VITE_SENTRY_DSN` still needs to be created by the user with a real Sentry project DSN for the test/production build; until then that build simply skips Sentry initialization and only console-logging is active (local development already has its own DSN via `.env.local`).
|
||||||
2. **Pick concrete analytics + uptime tools**: `monitoring-setup.md` lists candidate free-tier tools; final tool selection/account creation is a manual follow-up outside this workflow (no code changes required to swap providers, since neither is wired into the codebase yet beyond an optional embed snippet).
|
2. ~~Pick concrete analytics + uptime tools~~ — **Resolved**: self-hosted Umami + UptimeRobot (see `monitoring-setup.md` and `umami-setup.md`). Remaining manual follow-ups: actually deploying the Umami containers on the Pi, registering DNS/SSL for `analytics.slpsoftware.nl`, creating the UptimeRobot monitor, and setting the `VITE_UMAMI_SCRIPT_URL`/`VITE_UMAMI_WEBSITE_ID` Gitea repository variables.
|
||||||
|
|
||||||
## Rationale
|
## Rationale
|
||||||
Given this is a simple static marketing site with no backend and no existing monitoring, the aim is lightweight, low/no-cost observability: enough to know if the site is down (uptime) and how it's being used (analytics), plus a documented (if not yet finalized) path for capturing client-side errors. Alerting was deliberately left out to avoid over-engineering a notification pipeline before there's a concrete trigger/audience for it.
|
Given this is a simple static marketing site with no backend and no existing monitoring, the aim is lightweight, low/no-cost observability: enough to know if the site is down (uptime) and how it's being used (analytics), plus a documented (if not yet finalized) path for capturing client-side errors. Alerting was deliberately left out to avoid over-engineering a notification pipeline before there's a concrete trigger/audience for it.
|
||||||
|
|||||||
@@ -39,38 +39,50 @@ Both the console and Sentry are now active (original Question 3 resolved as a co
|
|||||||
|
|
||||||
## Dashboards
|
## Dashboards
|
||||||
|
|
||||||
### Website analytics
|
### Website analytics — decided: self-hosted Umami
|
||||||
Pick one (all have generous free tiers suitable for a small marketing site):
|
**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.
|
||||||
|
|
||||||
| Option | Notes |
|
- Full step-by-step setup: `operations/monitoring/umami-setup.md` (Podman/compose files,
|
||||||
|---|---|
|
systemd auto-start, reverse proxy + SSL, website registration).
|
||||||
| Plausible / Umami | Privacy-friendly, lightweight, no cookie banner typically required; self-hosted or low-cost hosted tier |
|
- Example config: `operations/deployment/umami/podman-compose.yml.example` +
|
||||||
| Google Analytics (GA4) / Search Console | Free, widely known, but heavier script and involves third-party data sharing (cookie/consent implications) |
|
`.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.
|
||||||
|
|
||||||
**Setup (once a tool is picked)**:
|
### Uptime dashboard — decided: UptimeRobot
|
||||||
1. Create an account/site entry with the chosen provider and obtain the tracking snippet or `<script>` tag.
|
**Decided**: UptimeRobot (free tier: up to 50 monitors, 5-minute check interval, optional
|
||||||
2. Add the snippet to `index.html` (or load it conditionally in `src/main.tsx`) — this is a documentation/config task, not something the current codebase needs restructuring for.
|
e-mail notification on downtime — opportunistic, not a designed alerting feature per
|
||||||
3. Key metrics to surface: unique visitors, page views per route (Home, Packages, etc. — see `frontend-components.md`), and referral sources.
|
`monitoring-plan.md`). Chosen over Better Uptime for its long-standing free tier and
|
||||||
|
simplicity for a single low-traffic site.
|
||||||
|
|
||||||
### Uptime dashboard
|
**Setup**:
|
||||||
Pick one:
|
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.
|
||||||
|
|
||||||
| Option | Notes |
|
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.
|
||||||
| 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`) |
|
|
||||||
| Better Uptime | Free tier available; similar capability, nicer public status page option |
|
|
||||||
|
|
||||||
**Setup (once a tool is picked)**:
|
|
||||||
1. Register the production URL (once hosting is finalized — see `operations/deployment/deployment-plan.md` "Open Item") as an HTTP(S) monitor, checking for a `200` response.
|
|
||||||
2. Optional: publish a public status page if desired for transparency to visitors.
|
|
||||||
3. Key metric to surface: uptime percentage / current status.
|
|
||||||
|
|
||||||
## Summary Table
|
## Summary Table
|
||||||
|
|
||||||
| Concern | Approach | Status |
|
| 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`) |
|
| 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 (Plausible/Umami/GA4) | Tool selection open item |
|
| 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/Better Uptime) | Tool selection + production URL open item |
|
| Site reachability | Uptime dashboard (UptimeRobot) | Tool decided; account creation + production URL are manual follow-ups |
|
||||||
| Alerting | Out of scope | Not configured |
|
| Alerting | Out of scope | Not configured |
|
||||||
| Shared infrastructure reuse | Out of scope | None exists yet |
|
| Shared infrastructure reuse | Out of scope | None exists yet |
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
# Umami Self-Hosted Setup (Analytics)
|
||||||
|
|
||||||
|
Stapsgewijze handleiding om Umami (self-hosted website-analytics) op te zetten op de
|
||||||
|
webserver-Pi (Pi Main, `192.168.1.103`) met Podman, en bereikbaar te maken via
|
||||||
|
`analytics.slpsoftware.nl`. Dit vult het "Analytics/uptime dashboard" open item uit
|
||||||
|
`operations/production-readiness-checklist.md` in voor het analytics-gedeelte.
|
||||||
|
|
||||||
|
Umami draait onder een **eigen dedicated Linux-user** (`umami`) in plaats van onder het
|
||||||
|
bestaande hoofdaccount, zodat elke self-hosted service (nu en in de toekomst) netjes
|
||||||
|
geïsoleerd blijft — zie "Waarom een dedicated user" hieronder. Dit patroon kan hergebruikt
|
||||||
|
worden voor volgende self-hosted diensten op dezelfde Pi('s).
|
||||||
|
|
||||||
|
## Vereisten
|
||||||
|
- Podman is al aanwezig op de Pi (bevestigd door de gebruiker) — systeembreed geïnstalleerd,
|
||||||
|
dus beschikbaar voor elke user, ook een nieuw aangemaakte.
|
||||||
|
- `podman-compose` geïnstalleerd voor de `umami`-user: `pip3 install --user podman-compose`
|
||||||
|
(of via de package manager van je distro, indien beschikbaar) — zie stap 0 hieronder.
|
||||||
|
- DNS: een A-record voor `analytics.slpsoftware.nl` dat naar het publieke IP van de
|
||||||
|
reverse-proxy-Pi wijst (dezelfde Pi die ook `test.slpsoftware.nl` afhandelt).
|
||||||
|
|
||||||
|
## Waarom een dedicated user
|
||||||
|
Podman zelf hoeft niet per user geïnstalleerd te worden (het is een systeembreed pakket),
|
||||||
|
maar rootless Podman-containers erven wél de rechten van de user die ze start. Door Umami
|
||||||
|
onder een eigen `umami`-user te draaien in plaats van je eigen hoofdaccount:
|
||||||
|
- kan een kwetsbaarheid in de Umami-container (zelfs bij een container-escape binnen de
|
||||||
|
rootless-namespace) geen bestanden van je persoonlijke account lezen/schrijven;
|
||||||
|
- houd je containerstorage, systemd user-services en logs van verschillende self-hosted
|
||||||
|
diensten netjes gescheiden per user, ook als je Pi qua rekenkracht beperkt is;
|
||||||
|
- kun je dit patroon straks 1-op-1 hergebruiken voor een volgende self-hosted dienst (bv.
|
||||||
|
een eigen `uptime`-user, `git`-user, etc.), zonder dat diensten elkaars bestanden kunnen
|
||||||
|
benaderen.
|
||||||
|
|
||||||
|
## 0. Dedicated `umami`-user aanmaken (op de webserver-Pi)
|
||||||
|
1. Maak de user aan (zonder wachtwoord-login is prima, we loggen in via `sudo -iu umami` of
|
||||||
|
`su - umami`):
|
||||||
|
```bash
|
||||||
|
sudo useradd --create-home --shell /bin/bash umami
|
||||||
|
sudo passwd -l umami # login met wachtwoord blokkeren, sudo -iu blijft werken
|
||||||
|
```
|
||||||
|
2. Controleer dat er een subuid/subgid-range is toegewezen (nodig voor rootless Podman).
|
||||||
|
Op de meeste moderne distributies (incl. Raspberry Pi OS) gebeurt dit automatisch bij
|
||||||
|
`useradd`:
|
||||||
|
```bash
|
||||||
|
grep umami /etc/subuid /etc/subgid
|
||||||
|
```
|
||||||
|
Zie je geen output, voeg dan handmatig een range toe (pas de startwaarde aan als die al
|
||||||
|
in gebruik is door een andere user):
|
||||||
|
```bash
|
||||||
|
sudo usermod --add-subuids 200000-265535 --add-subgids 200000-265535 umami
|
||||||
|
```
|
||||||
|
3. Zorg dat de `umami`-sessie blijft "linger-en", zodat rootless Podman-services ook
|
||||||
|
actief blijven zonder dat de user is ingelogd (nodig voor stap 2 hieronder):
|
||||||
|
```bash
|
||||||
|
sudo loginctl enable-linger umami
|
||||||
|
```
|
||||||
|
4. Log in als de nieuwe user om de rest van de setup uit te voeren:
|
||||||
|
```bash
|
||||||
|
sudo -iu umami
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. Umami + database opzetten (als de `umami`-user, op de webserver-Pi)
|
||||||
|
0. Installeer `podman-compose` voor deze user, indien nog niet systeembreed aanwezig:
|
||||||
|
```bash
|
||||||
|
pip3 install --user podman-compose
|
||||||
|
```
|
||||||
|
1. Maak een map aan, bv. `~/umami/` (dit is nu `/home/umami/umami/`), en kopieer daarin:
|
||||||
|
- `operations/deployment/umami/podman-compose.yml.example` → `~/umami/podman-compose.yml`
|
||||||
|
- `operations/deployment/umami/.env.example` → `~/umami/.env`
|
||||||
|
2. Vul in `~/umami/.env` een echte `POSTGRES_PASSWORD` en `APP_SECRET` in (bv. via
|
||||||
|
`openssl rand -base64 32` voor beide — gebruik twee verschillende waarden).
|
||||||
|
3. Start de containers:
|
||||||
|
```bash
|
||||||
|
cd ~/umami
|
||||||
|
podman-compose up -d
|
||||||
|
```
|
||||||
|
4. **Firewall (ufw) openzetten voor poort 3001.** Pi Main gebruikt `ufw` met als default
|
||||||
|
policy `deny incoming` — net als poort `3000` (Gitea) al een expliciete regel heeft
|
||||||
|
voor het interne IP van de reverse-proxy-Pi, heeft `3001` (Umami) dezelfde regel nodig,
|
||||||
|
anders krijg je bij het testen `curl: (56) Recv failure: Connection reset by peer`
|
||||||
|
(ook wanneer je *op Pi Main zelf* naar het publieke IP `192.168.1.103` curl't — dat
|
||||||
|
gaat namelijk nog steeds via de firewall-INPUT-chain, in tegenstelling tot
|
||||||
|
`localhost`/`127.0.0.1`):
|
||||||
|
```bash
|
||||||
|
sudo ufw allow from 192.168.1.102 to any port 3001 proto tcp
|
||||||
|
sudo ufw reload
|
||||||
|
```
|
||||||
|
Controleer met `sudo ufw status verbose` dat er nu een regel voor `3001/tcp` staat,
|
||||||
|
analoog aan de bestaande regel voor `3000/tcp`.
|
||||||
|
5. Controleer dat Umami draait en bereikbaar is op het interne netwerk. Let op: de
|
||||||
|
**externe** poort is `3001`, niet het gebruikelijke `3000` — die poort is op deze Pi al
|
||||||
|
in gebruik door Gitea (`bind: address already in use` bij het opstarten van de
|
||||||
|
container als je toch `3000` gebruikt):
|
||||||
|
```bash
|
||||||
|
curl http://192.168.1.103:3001/api/heartbeat
|
||||||
|
```
|
||||||
|
Dit zou een JSON-antwoord met `"ok"` moeten teruggeven. Krijg je toch een
|
||||||
|
`Connection reset by peer`, controleer dan eerst of `curl http://localhost:3001/api/heartbeat`
|
||||||
|
(dus via loopback, buiten de firewall om) wél werkt — zo ja, dan zit het probleem
|
||||||
|
zeker in de ufw-regel hierboven en niet in Umami/Podman zelf.
|
||||||
|
|
||||||
|
> **Let op — na een wijziging aan `podman-compose.yml`/`.env` (bv. een andere poort):**
|
||||||
|
> `podman-compose up -d` update alléén containers waarvan de configuratie is gewijzigd,
|
||||||
|
> maar een poortmapping (`ports:`) wordt door Podman **niet** live herladen op een
|
||||||
|
> bestaande, al aangemaakte container. Heb je `podman-compose up -d` al eerder gedraaid
|
||||||
|
> met een oude versie van `podman-compose.yml` (bv. met poort `3000` in plaats van
|
||||||
|
> `3001`), werk dan eerst je lokale `~/umami/podman-compose.yml` bij met de nieuwste
|
||||||
|
> versie uit dit repository, en draai daarna:
|
||||||
|
> ```bash
|
||||||
|
> cd ~/umami
|
||||||
|
> podman-compose down
|
||||||
|
> podman-compose up -d
|
||||||
|
> ```
|
||||||
|
> `podman-compose down` verwijdert de containers (niet de database-data, die staat in
|
||||||
|
> een persistent volume) en `up -d` maakt ze opnieuw aan met de bijgewerkte poort/config.
|
||||||
|
> Dit is enkel nodig als de container al bestond met de oude configuratie; bij een
|
||||||
|
> eerste, nieuwe `up -d` (of als de vorige poging überhaupt nooit is gestart doordat
|
||||||
|
> Podman de poort niet kon claimen) is dit niet nodig.
|
||||||
|
|
||||||
|
## 2. Automatisch starten na reboot (systemd user service)
|
||||||
|
Podman-compose start niet vanzelf op na een herstart van de Pi, tenzij je dit expliciet
|
||||||
|
instelt. Omdat rootless Podman hier al gebruikt wordt, is een systemd **user**-service —
|
||||||
|
gekoppeld aan de `umami`-user — de eenvoudigste aanpak. De `loginctl enable-linger umami`
|
||||||
|
uit stap 0.3 is hiervoor al gezet, dus deze service blijft ook draaien zonder dat de
|
||||||
|
`umami`-user zelf is ingelogd.
|
||||||
|
|
||||||
|
1. Log in (of blijf ingelogd) als de `umami`-user: `sudo -iu umami`.
|
||||||
|
|
||||||
|
> **Let op — "Failed to connect to bus: No medium found" bij `systemctl --user`:**
|
||||||
|
> Dit betekent dat er (nog) geen D-Bus/systemd user-sessie draait voor `umami`, meestal
|
||||||
|
> omdat `sudo -iu umami` geen volledige PAM-login-sessie registreert zoals een echte
|
||||||
|
> interactieve login dat wel doet — daardoor bestaat `/run/user/<uid>` (en de bus daarin)
|
||||||
|
> nog niet, zelfs met linger enabled. Los dit als volgt op:
|
||||||
|
> ```bash
|
||||||
|
> # Controleer eerst of linger daadwerkelijk actief staat:
|
||||||
|
> loginctl show-user umami | grep Linger
|
||||||
|
> # Verwacht: Linger=yes. Staat er Linger=no, herhaal dan:
|
||||||
|
> sudo loginctl enable-linger umami
|
||||||
|
> # Start de user-manager expliciet (uid van umami opzoeken met: id -u umami):
|
||||||
|
> sudo systemctl start user@$(id -u umami).service
|
||||||
|
> # Log opnieuw in als umami en zet XDG_RUNTIME_DIR expliciet, voor deze sessie:
|
||||||
|
> sudo -iu umami
|
||||||
|
> export XDG_RUNTIME_DIR=/run/user/$(id -u)
|
||||||
|
> ```
|
||||||
|
> Probeer daarna `systemctl --user daemon-reload` opnieuw. Blijft het misgaan, gebruik dan
|
||||||
|
> in plaats van `sudo -iu umami` het commando `sudo machinectl shell umami@ /bin/bash` om
|
||||||
|
> in te loggen — dat registreert wél altijd een volledige sessie inclusief D-Bus, in
|
||||||
|
> tegenstelling tot `sudo -iu`/`su -`.
|
||||||
|
|
||||||
|
2. Maak `~/.config/systemd/user/umami.service` aan:
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Umami analytics (podman-compose)
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=%h/umami
|
||||||
|
ExecStart=/usr/bin/podman-compose up
|
||||||
|
ExecStop=/usr/bin/podman-compose down
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
```
|
||||||
|
3. Activeer en start de service (nog steeds als de `umami`-user):
|
||||||
|
```bash
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now umami.service
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Reverse proxy + SSL (op de reverse-proxy-Pi)
|
||||||
|
1. Kopieer `operations/deployment/nginx/analytics-nginx.conf.example` (de versie van vóór
|
||||||
|
certbot: alleen poort 80, geen SSL) naar bv.
|
||||||
|
`/etc/nginx/sites-available/slpsoftware-analytics.conf` op de reverse-proxy-Pi, en maak
|
||||||
|
een symlink in `sites-enabled`.
|
||||||
|
2. Zorg dat het DNS-record voor `analytics.slpsoftware.nl` al actief is, en draai dan:
|
||||||
|
```bash
|
||||||
|
sudo certbot --nginx -d analytics.slpsoftware.nl
|
||||||
|
```
|
||||||
|
Certbot herschrijft het bestand automatisch met de HTTPS-configuratie en de
|
||||||
|
HTTP→HTTPS-redirect — zie `operations/deployment/nginx/analytics-nginx.conf.post-certbot.example`
|
||||||
|
voor hoe het er dan uitziet (referentie, niet zelf kopiëren).
|
||||||
|
3. Herlaad nginx: `sudo nginx -t && sudo systemctl reload nginx`.
|
||||||
|
4. Test: open `https://analytics.slpsoftware.nl` in de browser — je zou het
|
||||||
|
Umami-inlogscherm moeten zien (standaard inloggegevens: `admin` / `umami`, **direct
|
||||||
|
wijzigen na eerste login**).
|
||||||
|
|
||||||
|
## 4. Website registreren in Umami en het website-ID ophalen
|
||||||
|
1. Log in op `https://analytics.slpsoftware.nl` en wijzig direct het standaardwachtwoord.
|
||||||
|
2. Ga naar **Settings → Websites → Add website** en vul in:
|
||||||
|
- Name: `SLP Software` (of naar keuze)
|
||||||
|
- Domain: het domein van de daadwerkelijke website (bv. `slpsoftware.nl` of
|
||||||
|
`test.slpsoftware.nl`, afhankelijk van welke omgeving je eerst wilt meten)
|
||||||
|
3. Na het opslaan toont Umami een **Website ID** (een UUID) — dit heb je nodig voor de
|
||||||
|
volgende stap.
|
||||||
|
|
||||||
|
## 5. Tracking script koppelen aan de website (build-configuratie)
|
||||||
|
De React-app (`src/components/UmamiAnalytics.tsx`) injecteert het Umami tracking-script
|
||||||
|
automatisch, mits de volgende twee build-time variabelen zijn ingesteld — beide zijn
|
||||||
|
**geen secrets** (client-side zichtbaar), dus als Gitea Actions **repository variables**
|
||||||
|
(niet secrets), net als `VITE_SENTRY_DSN`:
|
||||||
|
|
||||||
|
| Variabele | Waarde |
|
||||||
|
|---|---|
|
||||||
|
| `VITE_UMAMI_SCRIPT_URL` | `https://analytics.slpsoftware.nl/script.js` |
|
||||||
|
| `VITE_UMAMI_WEBSITE_ID` | het Website ID uit stap 4 |
|
||||||
|
|
||||||
|
Stel deze in via **Gitea → Repository Settings → Actions → Variables**. Zodra beide
|
||||||
|
bestaan, pakt de eerstvolgende build ze automatisch op; zonder deze variabelen slaat de
|
||||||
|
app het inladen van het script gewoon over (geen crash, geen tracking).
|
||||||
|
|
||||||
|
**Lokaal (`pnpm dev`)**: het tracking-script wordt hier bewust nooit geladen (zie
|
||||||
|
`UmamiAnalytics.tsx`), zodat lokaal testen de bezoekersstatistieken niet vervuilt. Wil je
|
||||||
|
dit toch lokaal testen, zet dan tijdelijk beide waarden in `.env.local` (zie
|
||||||
|
`.env.example`) én verwijder tijdelijk de `import.meta.env.DEV`-check.
|
||||||
|
|
||||||
|
## Openstaande handmatige stappen
|
||||||
|
- [ ] Dedicated `umami`-user aanmaken op Pi Main (incl. subuid/subgid-check en `loginctl enable-linger`).
|
||||||
|
- [ ] `podman-compose up -d` daadwerkelijk draaien als de `umami`-user op Pi Main.
|
||||||
|
- [ ] ufw-regel toevoegen voor poort `3001/tcp` (analoog aan de bestaande `3000/tcp`-regel) op Pi Main.
|
||||||
|
- [ ] Systemd user-service instellen (onder de `umami`-user) voor auto-start na reboot.
|
||||||
|
- [ ] DNS-record + certbot voor `analytics.slpsoftware.nl` op de reverse-proxy-Pi.
|
||||||
|
- [ ] Standaard Umami-wachtwoord direct wijzigen na eerste login.
|
||||||
|
- [ ] Website aanmaken in Umami en het Website ID overnemen.
|
||||||
|
- [ ] `VITE_UMAMI_SCRIPT_URL` en `VITE_UMAMI_WEBSITE_ID` als Gitea repository variables instellen.
|
||||||
|
|
||||||
|
## Vervolgstappen voor toekomstige self-hosted diensten
|
||||||
|
Dit dedicated-user-patroon (stap 0 hierboven) is bewust generiek gehouden zodat het
|
||||||
|
hergebruikt kan worden: een volgende self-hosted dienst op dezelfde of een andere Pi kan
|
||||||
|
op dezelfde manier zijn eigen user krijgen (bv. `useradd --create-home`, subuid/subgid
|
||||||
|
controleren, `loginctl enable-linger <user>`, eigen `~/.config/systemd/user/<dienst>.service`),
|
||||||
|
zodat diensten onderling geïsoleerd blijven zonder dat dit ten koste gaat van de
|
||||||
|
al beperkte rekenkracht van een Raspberry Pi (rootless Podman zelf blijft immers
|
||||||
|
systeembreed gedeeld, alleen de user-context verandert per dienst).
|
||||||
@@ -20,5 +20,5 @@
|
|||||||
- **Open Follow-ups**:
|
- **Open Follow-ups**:
|
||||||
- Finalize the hosting/domain setup and extend `.gitea/workflows/deploy.yml`'s `deploy` job to actually upload `dist/` to the host, instead of only packaging it as a downloadable artifact (see `deployment-plan.md` "Open Item")
|
- Finalize the hosting/domain setup and extend `.gitea/workflows/deploy.yml`'s `deploy` job to actually upload `dist/` to the host, instead of only packaging it as a downloadable artifact (see `deployment-plan.md` "Open Item")
|
||||||
- ~~Decide the client-side error logging destination~~ — **Resolved and verified**: console + Sentry free tier, implemented and confirmed working end-to-end (errors, logs, metrics) locally and on the test environment, including tracing, environment/release tags, and an ad-blocker-proof tunnel (see `monitoring-setup.md`); remaining manual step is creating the `VITE_SENTRY_DSN` Gitea Actions variable for the test/production build
|
- ~~Decide the client-side error logging destination~~ — **Resolved and verified**: console + Sentry free tier, implemented and confirmed working end-to-end (errors, logs, metrics) locally and on the test environment, including tracing, environment/release tags, and an ad-blocker-proof tunnel (see `monitoring-setup.md`); remaining manual step is creating the `VITE_SENTRY_DSN` Gitea Actions variable for the test/production build
|
||||||
- Pick and configure the concrete analytics tool (e.g. Plausible/Umami/GA4) and uptime dashboard tool (e.g. UptimeRobot/Better Uptime), including registering the final production URL once hosting is finalized
|
- ~~Pick and configure the concrete analytics tool and uptime dashboard tool~~ — **Resolved**: self-hosted Umami (Podman on Pi Main) + UptimeRobot decided; see `monitoring-setup.md` and the new `umami-setup.md`. Remaining manual follow-ups: actually deploy the Umami containers, set up `analytics.slpsoftware.nl` DNS/SSL, create the UptimeRobot monitor, and register the final production URL once hosting is finalized
|
||||||
- Run a formal Lighthouse performance check before the first real production deployment (flagged as not yet run in `build-and-test-summary.md`)
|
- Run a formal Lighthouse performance check before the first real production deployment (flagged as not yet run in `build-and-test-summary.md`)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { Nav } from './Nav';
|
import { Nav } from './Nav';
|
||||||
import { Footer } from './Footer';
|
import { Footer } from './Footer';
|
||||||
|
import { UmamiAnalytics } from './UmamiAnalytics';
|
||||||
import { SentryTestButton } from './SentryTestButton';
|
import { SentryTestButton } from './SentryTestButton';
|
||||||
|
|
||||||
export function RootLayout({ children }: { children: ReactNode }) {
|
export function RootLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="font-sans min-h-screen bg-bg text-text">
|
<div className="font-sans min-h-screen bg-bg text-text">
|
||||||
|
<UmamiAnalytics />
|
||||||
<Nav />
|
<Nav />
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
const SCRIPT_ELEMENT_ID = 'umami-analytics-script';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects the self-hosted Umami tracking script (see
|
||||||
|
* aidlc-docs/features/react-frontend/operations/monitoring/umami-setup.md) when both
|
||||||
|
* VITE_UMAMI_SCRIPT_URL and VITE_UMAMI_WEBSITE_ID are configured at build time.
|
||||||
|
*
|
||||||
|
* Silently does nothing if either variable is missing (e.g. local development, or a
|
||||||
|
* build for which the Umami instance/website has not been set up yet) — safe default,
|
||||||
|
* no crash on missing config, mirroring the existing Sentry DSN pattern.
|
||||||
|
*
|
||||||
|
* Skipped during local development (`pnpm dev`) by default so local testing does not
|
||||||
|
* pollute production/test visitor analytics.
|
||||||
|
*/
|
||||||
|
export function UmamiAnalytics() {
|
||||||
|
useEffect(() => {
|
||||||
|
const scriptUrl = import.meta.env.VITE_UMAMI_SCRIPT_URL;
|
||||||
|
const websiteId = import.meta.env.VITE_UMAMI_WEBSITE_ID;
|
||||||
|
|
||||||
|
if (import.meta.env.DEV || !scriptUrl || !websiteId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.getElementById(SCRIPT_ELEMENT_ID)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.id = SCRIPT_ELEMENT_ID;
|
||||||
|
script.src = scriptUrl;
|
||||||
|
script.defer = true;
|
||||||
|
script.setAttribute('data-website-id', websiteId);
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.getElementById(SCRIPT_ELEMENT_ID)?.remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { render, cleanup } from '@testing-library/react';
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { UmamiAnalytics } from '../UmamiAnalytics';
|
||||||
|
|
||||||
|
const SCRIPT_ID = 'umami-analytics-script';
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
document.getElementById(SCRIPT_ID)?.remove();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('UmamiAnalytics', () => {
|
||||||
|
it('does not inject a script tag when the env vars are missing', () => {
|
||||||
|
vi.stubEnv('VITE_UMAMI_SCRIPT_URL', '');
|
||||||
|
vi.stubEnv('VITE_UMAMI_WEBSITE_ID', '');
|
||||||
|
|
||||||
|
render(<UmamiAnalytics />);
|
||||||
|
|
||||||
|
expect(document.getElementById(SCRIPT_ID)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not inject a script tag during local development, even if configured', () => {
|
||||||
|
vi.stubEnv('DEV', true);
|
||||||
|
vi.stubEnv('VITE_UMAMI_SCRIPT_URL', 'https://analytics.slpsoftware.nl/script.js');
|
||||||
|
vi.stubEnv('VITE_UMAMI_WEBSITE_ID', 'test-website-id');
|
||||||
|
|
||||||
|
render(<UmamiAnalytics />);
|
||||||
|
|
||||||
|
expect(document.getElementById(SCRIPT_ID)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects the tracking script with the configured URL and website id when built for test/production', () => {
|
||||||
|
vi.stubEnv('DEV', false);
|
||||||
|
vi.stubEnv('VITE_UMAMI_SCRIPT_URL', 'https://analytics.slpsoftware.nl/script.js');
|
||||||
|
vi.stubEnv('VITE_UMAMI_WEBSITE_ID', 'test-website-id');
|
||||||
|
|
||||||
|
render(<UmamiAnalytics />);
|
||||||
|
|
||||||
|
const script = document.getElementById(SCRIPT_ID) as HTMLScriptElement | null;
|
||||||
|
expect(script).not.toBeNull();
|
||||||
|
expect(script?.src).toBe('https://analytics.slpsoftware.nl/script.js');
|
||||||
|
expect(script?.getAttribute('data-website-id')).toBe('test-website-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders nothing visible', () => {
|
||||||
|
const { container } = render(<UmamiAnalytics />);
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
});
|
||||||
Vendored
+4
@@ -1,6 +1,10 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
|
/** Umami tracking script URL (self-hosted), e.g. https://analytics.slpsoftware.nl/script.js */
|
||||||
|
readonly VITE_UMAMI_SCRIPT_URL?: string;
|
||||||
|
/** Umami website ID, created manually in the Umami dashboard for this site. */
|
||||||
|
readonly VITE_UMAMI_WEBSITE_ID?: string;
|
||||||
/** Sentry DSN (Data Source Name) for client-side error reporting. Not a secret — safe to expose in the client bundle. */
|
/** Sentry DSN (Data Source Name) for client-side error reporting. Not a secret — safe to expose in the client bundle. */
|
||||||
readonly VITE_SENTRY_DSN?: string;
|
readonly VITE_SENTRY_DSN?: string;
|
||||||
/** Build-time deployment environment tag ('test' | 'production' | undefined). Used to hide dev/test-only UI (e.g. SentryTestButton) from production builds. */
|
/** Build-time deployment environment tag ('test' | 'production' | undefined). Used to hide dev/test-only UI (e.g. SentryTestButton) from production builds. */
|
||||||
|
|||||||
Reference in New Issue
Block a user