https://github.com/overshard/analytics
Single-binary self-hosted website analytics. Collector API, dashboards, world map, PDF reports.
https://github.com/overshard/analytics
alpine analytics axum docker rust self-hosted sqlite vite
Last synced: about 2 months ago
JSON representation
Single-binary self-hosted website analytics. Collector API, dashboards, world map, PDF reports.
- Host: GitHub
- URL: https://github.com/overshard/analytics
- Owner: overshard
- License: bsd-2-clause
- Created: 2022-06-16T00:59:24.000Z (about 4 years ago)
- Default Branch: master
- Last Pushed: 2026-05-07T22:59:03.000Z (about 2 months ago)
- Last Synced: 2026-05-08T00:34:35.762Z (about 2 months ago)
- Topics: alpine, analytics, axum, docker, rust, self-hosted, sqlite, vite
- Language: Rust
- Homepage: https://analytics.bythewood.me
- Size: 1.32 MB
- Stars: 1
- Watchers: 1
- Forks: 1
- Open Issues: 6
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
Awesome Lists containing this project
README
# Analytics
Self-hosted website analytics, single-operator. One axum binary with sqlx + SQLite, minijinja templates, and a Vite + Bootstrap frontend. Tracks page views, clicks, scrolls, sessions, and custom events; renders metric cards, time-series charts, a world map with admin-1 drill-down, and PDF/markdown reports for any date range.
## Features
- Embed a small collector script on any number of properties (tracked sites)
- Dashboard with 16 metric/chart/list aggregations and user-defined custom cards
- GeoIP enrichment (auto-downloads DB-IP City Lite) and UA parsing (auto-downloads ua-parser regexes)
- Bot traffic routed to a separate table so human dashboards never have to filter it
- Public/private toggle per property; signed-cookie auth for the operator
- PDF and markdown export of any dashboard view (Chromium-rendered PDF)
- Single-binary deploy via `git push server master`
## System dependencies
Local dev needs all of these on your `PATH`:
| Tool | Why | Version |
|---|---|---|
| `rustc` / `cargo` | Build the axum binary | 2021 edition, current stable is fine (1.70+) |
| `bun` | Frontend deps + Vite + map builder | 1.x |
| `make` | Run the dev/build targets | any |
| `pkg-config` + OpenSSL headers | Linked at build time on Linux | distro packages: `pkg-config`, `libssl-dev` (Debian/Ubuntu), `openssl-dev` (Alpine) |
| `chromium` | PDF report export only; everything else works without it | any recent build |
Install hints:
```sh
# Rust toolchain (recommended via rustup)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Bun
curl -fsSL https://bun.sh/install | bash
# System libs (Debian/Ubuntu)
sudo apt install -y build-essential pkg-config libssl-dev chromium
# System libs (Alpine)
sudo apk add musl-dev pkgconfig openssl-dev chromium
```
The Docker build (see `Dockerfile`) reproduces this on `rust:alpine` + `alpine:3.23`. If you only care about Docker, you do not need any of the above on the host.
Two more files are downloaded into `data/` at runtime, no setup required:
- `data/db.mmdb`: DB-IP City Lite (CC-BY-4.0). Refreshed if older than 30 days.
- `data/regexes.yaml`: canonical ua-parser regexes (Apache-2.0).
If either download fails the server still boots: UA parsing falls back to a substring heuristic and GeoIP enrichment is skipped.
## Quickstart
```sh
cp samplefiles/env.sample .env
# edit .env to set ANALYTICS_PASSWORD and BASE_URL
make
```
`make` (alias `make run`) installs frontend deps if needed, then runs Vite watch and `cargo run` concurrently on port 8000. Visit http://localhost:8000/login.
First boot creates a "Proprium" property and starts tracking the dashboard's own usage.
## Configuration
All config comes from `.env` (loaded via `dotenvy`). The full set:
| Variable | Required | Purpose |
|---|---|---|
| `ANALYTICS_PASSWORD` | yes | Single operator password |
| `BASE_URL` | yes for prod | Used in absolute URLs (sitemap, og tags, embed snippet) |
| `PORT` | no (default `8000`) | HTTP listen port |
| `ANALYTICS_COOKIE_SECRET` | no | 32+ bytes for signing the session cookie. Falls back to a SHA-512 of the password, so rotating the password invalidates sessions |
| `ANALYTICS_DATA_DIR` | no (default `./data`) | Where the SQLite db, mmdb, and regexes live. Production sets this to `/data` |
| `ANALYTICS_ROOT` | no | Override the project root (where `templates/`, `dist/`, `migrations/`, `static_maps/` are read from) |
| `CHROMIUM_BIN` | no | Path to chromium for PDF export. Falls back to `PATH` lookup, then a `/opt/playwright-browsers/` glob |
## Make targets
| Target | What it does |
|---|---|
| `make run` (default) | Vite watch + `cargo run` on port 8000 |
| `make build` | Vite assets + topojson maps + release binary (`target/release/analytics`) |
| `make start` | Run the release binary (after `make build`) |
| `make maps` | Rebuild per-country topojson under `static_maps/` from Natural Earth |
| `make seed` | Create or refresh a "Seed Test" property with realistic fake events. Override with `SESSIONS=2000 DAYS=60` |
| `make migrate FROM=` | One-shot import of an existing Django analytics database, preserving property UUIDs so embedded snippets keep working. Add `FORCE=1` to wipe first |
| `make pull` | rsync the production db + geoip from `git remote server` into `data/` |
| `make push` | `git push` to every configured remote |
| `make clean` | `cargo clean` plus removing `dist/`, `node_modules/`, the SQLite db, and the mmdb |
There are no tests or linters configured.
## Deploy
Production runs on Docker. The standard flow is `git push server master` to a remote whose post-receive hook runs `docker compose up --build --detach`. Sample files in `samplefiles/`:
- `Caddyfile.sample`: reverse proxy with TLS
- `env.sample`: the same `.env` shown above
- `post-receive.sample`: the git hook
Data persists to `/srv/data/analytics/` on the host (mounted into the container at `/data`).
## Stack
- **Backend:** axum 0.8, sqlx 0.8 against SQLite (WAL, `synchronous=NORMAL`, `busy_timeout=5s`), tower-cookies for signed sessions
- **Templates:** minijinja 2 with a Jinja2-faithful HTML formatter
- **Frontend:** Vite 6, Bootstrap 5 SCSS, Chart.js, d3-geo + topojson, monaspace argon font (self-hosted via `@fontsource`)
- **Enrichment:** maxminddb (GeoIP), uaparser (UA), moka (5-minute dashboard cache)
- **PDF:** chrome-headless-shell via `--print-to-pdf` against a temp html file
See [CLAUDE.md](CLAUDE.md) for the full architecture rundown, route table, and data model.