storebound/docs/site-analytics.md

57 lines
3.9 KiB
Markdown

# storebound.dev site analytics
The site counts visits with a self-hosted [Umami](https://umami.is) on the VDS that serves storebound.dev. Cookieless,
no personal data, Do Not Track respected, nothing sent to a third party. This page is the rebuild recipe (no secrets).
## What runs where
| Piece | Where |
|---|---|
| Umami 3.4.0 + Postgres 15.14 (Docker Compose, `restart: unless-stopped`) | `/opt/umami` on the VDS, `127.0.0.1:3920` only |
| Secrets (DB password, `APP_SECRET`, `TWO_FACTOR_ENCRYPTION_KEY`) | `/opt/umami/.env`, mode 600, generated on the server |
| Script and collect endpoint, first-party | `https://storebound.dev/s/p.js`, `https://storebound.dev/s/e` (`/etc/apache2/storebound-stats.conf`, from `tools/vds/apache-storebound-stats.conf`, included in the storebound.dev :443 vhost) |
| Dashboard | `https://stats.crt.fyi` (`tools/vds/apache-stats.crt.fyi.conf`; `*.crt.fyi` wildcard DNS through Cloudflare and the `crt.fyi` wildcard cert) |
| Admin login | the owner's Mac, `~/.storebound-keys/umami.json` (0600) |
URLs never contain "umami", "analytics", "track" or "collect" (`TRACKER_SCRIPT_NAME=p.js`, `COLLECT_API_ENDPOINT=/e`), so
common filter lists do not block them.
**Client IP.** Cloudflare sits in front. Apache's `mod_remoteip` takes `CF-Connecting-IP` only from Cloudflare's ranges
and passes the result as `X-Sb-Client`, which Umami reads (`CLIENT_IP_HEADER=x-sb-client`). A client cannot spoof it by
sending the header to the origin directly. The collect endpoint answers `Cache-Control: no-store`.
## Rebuild
1. `/opt/umami/docker-compose.yml`: Umami's upstream compose with the image pinned to `ghcr.io/umami-software/umami:3.4.0`,
port `127.0.0.1:3920:3000`, `env_file: .env`, and `TRACKER_SCRIPT_NAME: p.js`, `COLLECT_API_ENDPOINT: /e`,
`CLIENT_IP_HEADER: x-sb-client`, `DISABLE_TELEMETRY: "1"`, `DISABLE_UPDATES: "1"`; Postgres `postgres:15.14-alpine` with a
named volume, both `restart: unless-stopped`.
2. `.env` (umask 077): `POSTGRES_PASSWORD`, `DATABASE_URL=postgresql://umami:<pw>@db:5432/umami`, `APP_SECRET` and
`TWO_FACTOR_ENCRYPTION_KEY` from `openssl rand -hex`. `docker compose up -d`, wait for `/api/heartbeat`.
3. Log in as `admin` / `umami`, change the password at once (`POST /api/me/password`) to a generated one and store it in
`~/.storebound-keys/umami.json` on the Mac. Create the website `storebound.dev`; its id goes into the script tag.
4. Apache (back up every file first, `cp -a <file> /root/<name>.bak-<timestamp>`). The Cloudflare ranges live only on
the server, in `/etc/apache2/cloudflare-trusted-proxies.conf`; refresh them now and then:
```sh
ssh root@<server> 'for u in ips-v4 ips-v6; do curl -fs https://www.cloudflare.com/$u | sed "s/^/RemoteIPTrustedProxy /"; echo; done | grep . > /etc/apache2/cloudflare-trusted-proxies.conf'
scp tools/vds/apache-storebound-stats.conf root@<server>:/etc/apache2/storebound-stats.conf
scp tools/vds/apache-stats.crt.fyi.conf root@<server>:/etc/apache2/sites-available/stats.crt.fyi.conf
# in the storebound.dev :443 vhost, after the desktop include: Include /etc/apache2/storebound-stats.conf
ssh root@<server> 'a2ensite stats.crt.fyi && apache2ctl configtest && systemctl reload apache2'
```
5. Site: `site/index.html` loads `https://storebound.dev/s/p.js` with `defer`, `data-host-url="https://storebound.dev/s"`,
`data-domains="storebound.dev"` (localhost and previews send nothing) and `data-do-not-track="true"`.
## Events (site/main.js)
| Event | Data |
|---|---|
| `copy` | `what`: the copied command or the copied block's id (every Copy button) |
| `desktop-download` | `os` (macos, windows, linux), `file` |
| `teaser-play`, `teaser-50`, `teaser-complete` | once per page view |
| `outbound` | `kind` (docs, git, npm, other), `host`, `path` |
## Upgrade
Read Umami's release notes, change the pinned tag, `docker compose pull && docker compose up -d` in `/opt/umami`
(take `docker compose exec db pg_dump -U umami umami > /root/umami-<date>.sql` first).