diff --git a/ansible/files/Caddyfile.j2 b/ansible/files/Caddyfile.j2 index 2e276b7..ac2560a 100644 --- a/ansible/files/Caddyfile.j2 +++ b/ansible/files/Caddyfile.j2 @@ -8,10 +8,11 @@ reverse_proxy {{ docker_container }}:{{ cms_port }} } - # First-party analytics (Vince) — script (/stats/js/script.js) and event - # endpoint (/stats/api/event) proxied same-origin so they survive ad - # blockers and never touch a third-party domain. - handle /stats/* { + # First-party analytics (Vince) — the tracker script and event endpoint + # are proxied same-origin with the /stats prefix stripped (vince serves + # /js/script.js and /api/event), so they survive ad blockers and never + # touch a third-party domain. + handle_path /stats/* { reverse_proxy {{ vince_container }}:{{ vince_port }} } @@ -25,3 +26,10 @@ redir / /_/ reverse_proxy {{ docker_container }}:{{ cms_port }} } + +# Vince analytics dashboard (login-protected). DNS for this host must point +# at this server; the tracker script itself stays on the www host at +# /stats/* — this is only for humans reading the dashboard. +{{ stats_host }} { + reverse_proxy {{ vince_container }}:{{ vince_port }} +} diff --git a/ansible/roles/analytics/defaults/main.yaml b/ansible/roles/analytics/defaults/main.yaml index 0444bb3..0011b23 100644 --- a/ansible/roles/analytics/defaults/main.yaml +++ b/ansible/roles/analytics/defaults/main.yaml @@ -1,4 +1,5 @@ --- -# vince_container / vince_port live in ansible/vars/default.yml (the Caddy -# template needs them at play level). +# vince_container / vince_port / vince_admin_* / vince_site_domain / +# stats_host live in ansible/vars/default.yml (the Caddy template needs them +# at play level). vince_image: ghcr.io/vinceanalytics/vince:latest diff --git a/ansible/roles/analytics/tasks/main.yaml b/ansible/roles/analytics/tasks/main.yaml index b743705..24431ba 100644 --- a/ansible/roles/analytics/tasks/main.yaml +++ b/ansible/roles/analytics/tasks/main.yaml @@ -1,7 +1,12 @@ --- # Self-hosted first-party analytics (Vince — Plausible-protocol, single -# binary). Only reachable on the docker network: Caddy proxies /stats/* -# on the www host to this container, so browser requests stay first-party. +# binary). Only reachable on the docker network: +# - Caddy proxies /stats/* on the www host here (prefix stripped), serving +# the tracker script and event API first-party; +# - Caddy publishes the dashboard on {{ stats_host }}. +# +# The image's ENTRYPOINT is the bare /vince binary (prints help and exits +# with no args), so the container MUST be given the `serve` command. - name: Pull Vince image docker_image: name: "{{ vince_image }}" @@ -20,8 +25,19 @@ docker_container: name: "{{ vince_container }}" image: "{{ vince_image }}" + command: serve state: started restart_policy: "unless-stopped" + env: + VINCE_DATA: /data + VINCE_LISTEN: "0.0.0.0:{{ vince_port }}" + # serve (re)creates the admin account from these on every boot — + # the env file is the source of truth, like the PocketBase superuser. + VINCE_ADMIN_NAME: "{{ vince_admin_name }}" + VINCE_ADMIN_PASSWORD: "{{ vince_admin_password }}" + # create the tracked site on startup (CSV list) + VINCE_DOMAINS: "{{ vince_site_domain }}" + VINCE_URL: "https://{{ stats_host }}" volumes: - "{{ host_directory }}/vince_data:/data" networks: diff --git a/ansible/vars/default.yml b/ansible/vars/default.yml index 81ff901..f11d8aa 100644 --- a/ansible/vars/default.yml +++ b/ansible/vars/default.yml @@ -20,9 +20,15 @@ docker_network: "{{ title }}_net" docker_image: "{{ registry }}/{{ title }}:{{ image_tag | default(tag) }}" docker_container: "{{ title }}-staging" -# self-hosted first-party analytics (Vince) — proxied at /stats/* on www host +# self-hosted first-party analytics (Vince) — script/events proxied at +# /stats/* on the www host, dashboard published on stats_host vince_container: vince vince_port: 8000 +vince_admin_name: admin +# change this before first deploy — it (re)sets the dashboard login +vince_admin_password: "ChangeMe-Vince-2025!" +vince_site_domain: mozimo.in +stats_host: stats.mozimo.in # caddy publishes www (SSR + /api/* + /_/* -> pocketbase) and cms (admin) diff --git a/docs/analytics-setup.md b/docs/analytics-setup.md index ab8c78e..21c794a 100644 --- a/docs/analytics-setup.md +++ b/docs/analytics-setup.md @@ -72,13 +72,19 @@ Shopify (shop.mozimo.in) ─ purchase/checkout ──► same GA4 property ### 2.4 Vince (first-party layer) -1. `./deploy.sh` once (the playbook creates the container + Caddy route). -2. Open `https:///stats/` → register the admin account (first - visit only). Then **Add a website** with domain `mozimo.in`. +1. `./deploy.sh` once — the playbook creates the container (`command: serve`) + with `VINCE_ADMIN_NAME` / `VINCE_ADMIN_PASSWORD` from + `ansible/vars/default.yml` (change the password there before deploying) + and auto-creates the `mozimo.in` site via `VINCE_DOMAINS`. +2. Add a DNS record for `stats.mozimo.in` (same server IP) — Caddy publishes + the dashboard there; log in at `https://stats.mozimo.in/login` with the + admin name/password from the ansible vars. 3. Set `VITE_PLAUSIBLE_DOMAIN=mozimo.in` in your local `.env` and redeploy — - the script tag is injected only when this is set. + the script tag (with `data-api="/stats/api/event"`) is injected only when + this is set. 4. Check `https:///stats/js/script.js` returns the script (200) - and that browsing the site populates the dashboard within seconds. + and that browsing the site populates `stats.mozimo.in/` within + seconds. 5. With uBlock Origin enabled: visits still appear in Vince but not GA4 — expected and exactly why this layer exists. diff --git a/src/app/__root.tsx b/src/app/__root.tsx index 8feb1eb..f52ba97 100644 --- a/src/app/__root.tsx +++ b/src/app/__root.tsx @@ -127,6 +127,9 @@ function analyticsHeadScripts() { src: "/stats/js/script.js", defer: true, "data-domain": PLAUSIBLE_DOMAIN, + // the tracker derives its event endpoint from the script origin + // by default; point it at the proxied path explicitly + "data-api": "/stats/api/event", }, ] : []),