# 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:@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 /root/.bak-`). The Cloudflare ranges live only on the server, in `/etc/apache2/cloudflare-trusted-proxies.conf`; refresh them now and then: ```sh ssh root@ '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@:/etc/apache2/storebound-stats.conf scp tools/vds/apache-stats.crt.fyi.conf root@:/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@ '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-.sql` first).