storebound/docs/site-analytics.md

3.9 KiB

storebound.dev site analytics

The site counts visits with a self-hosted Umami 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:
    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).