Self-hostable website analytics on Django: a straightforward collector API, dashboards, a world map, and PDF reports.
analyticsdjangodockerhandcodedpythonself-hostedsqliteviteweb-analytics
1# CLAUDE.md
2
3This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
5## What This Is
6
7Self-hosted website analytics service. Tracks page views, clicks, scrolls, sessions, and custom events. Users create "Properties" (tracked sites), embed a collector script, and view dashboards with date range filtering, comparisons, charts, maps, and PDF report export.
8
9## Development Commands
10
11- **`make`** — installs deps (uv + bun) if needed, creates DB, then runs Django dev server and Vite watch concurrently
12- **`uv run python manage.py runserver`** — Django only
13- **`bun run dev`** — Vite watch only
14- **`uv run python manage.py migrate`** — apply migrations
15- **`uv run python manage.py makemigrations`** — generate migrations
16- **`make pull`** — rsync production DB, GeoIP database, and media from server
17- **`make push`** — push to all git remotes
18- **Deps:** Python via `uv`, JS via `bun`. No tests, no linters.
19- **Default login:** admin / admin
20
21## Django Apps
22
23| App | Purpose |
24|---|---|
25| `analytics` | Project config (settings, URLs, templates, ASGI/WSGI, context processors, headless chromium PDF utils via `analytics/chromium.py`). Vite outputs land in `analytics/static/`. |
26| `accounts` | Custom `User` model (UUID PK, extends `AbstractUser`). Login/logout/signup views. |
27| `properties` | Core domain. `Property` model (a tracked site, UUID PK, belongs to User) and `Event` model (stores all analytics events as JSON in `data` field). Dashboard views, query helpers (`queries.py`), and custom card management. |
28| `collector` | Single `POST /collect/` endpoint (CSRF-exempt, CORS-open). Receives events from client JS, enriches with GeoIP + user-agent parsing, filters bots, saves to DB. |
29| `pages` | Static pages: home, changelog, documentation, favicon, robots, sitemap. |
30
31## Architecture
32
33**Data model:** Everything centers on `Property` → `Event`. Events have a `event` type string (session_start, page_view, click, scroll, page_leave, or custom) and a `data` JSONField holding all event-specific key-value pairs. There is no separate table per event type — all querying is done via Django's JSON field lookups (`data__url`, `data__referrer`, `data__utm_source`, etc.).
34
35**Collection flow:** Client sites include `collector.js` (bundled via Vite's `collector` entry point). The script sets a `collectoruserid` cookie, fires session_start (on first visit), page_view, click, scroll, and page_leave events to `POST /collect/`. The server-side view enriches session_start events with GeoIP data (uses `db.mmdb`, auto-downloaded on container start by the `refresh_geoip` management command from DB-IP City Lite — CC-BY-4.0, no signup) and parses user-agent strings into platform/browser/device fields. Bot traffic is silently dropped.
36
37**Dashboard:** `properties/views.py:property()` is the main dashboard view. It filters events by date range, computes current vs. previous period comparisons, and builds all chart/list data server-side. Standard metric cards are computed in `properties/queries.py`. Properties can have custom event cards (stored as JSON on the Property model). The dashboard supports a `?report` query param to generate PDF reports via a headless Chromium subprocess (`analytics/chromium.py`).
38
39**Frontend:** Vite bundles 4 entry points (`base`, `pages`, `properties`, `collector`) from each app's `static_src/` directory. Uses Bootstrap 5 (SCSS), Chart.js for graphs, `d3-geo` + `topojson-client` for the world map. Output goes to `analytics/static/`. WhiteNoise serves static files.
40
41**Map data:** `analytics/scripts/build_maps.js` (run via `bun run build:maps` at Docker build time) downloads Natural Earth admin-0 110m + admin-1 10m GeoJSON, encodes as TopoJSON, and writes per-country files to `analytics/static_maps/` (added to `STATICFILES_DIRS`). The world view is always loaded; per-country admin-1 (states/provinces) is lazy-fetched on click. Attribution: DB-IP link in the footer, Natural Earth is public domain.
42
43**Settings:** Split into `analytics/settings/__init__.py` (shared), `development.py`, and `production.py`. Dev uses SQLite at project root; production uses SQLite at `/data/db/db.sqlite3`. `DJANGO_SETTINGS_MODULE` defaults to development; production sets it via `.env`.
44
45**Production:** Single Docker container (Alpine 3.21 base) running Gunicorn with Uvicorn workers (ASGI). Caddy as reverse proxy. Data persisted to `/srv/data/analytics/`. Deployed via `git push server master` triggering a post-receive hook.
46
47## Key Conventions
48
49- All model PKs are UUIDs.
50- Event data is schemaless — the `data` JSONField is the extensibility point. New event attributes are added by sending them from the client; no migration needed.
51- The `collector` context processor injects `collector_server` and `collector_id` into all templates so the app can track its own usage (property named "Proprium").
52- GeoIP is auto-downloaded on container start (DB-IP City Lite). Monthly refresh via host cron (`docker exec analytics_web python manage.py refresh_geoip --force`). If the file is missing or stale, the collector silently skips enrichment — non-fatal.
53- Chromium is bundled in the Docker image (Alpine `chromium` package) for server-side PDF generation. `analytics/chromium.py` wraps a headless Chromium subprocess — no Playwright dependency.