EU Stats is a self-hosted web analytics project for people who want a simple way to watch traffic on their own site without turning it into a large marketing stack.
In its current form, the project focuses on pageview-oriented analytics:
- pageviews
- top pages
- referrers
- custom event reporting
- a small site management dashboard
The bundled tracker is intentionally small, but it is not storage-free in its current form. It does not use cookies and it does not use localStorage, but it does use sessionStorage to keep a per-tab identifier for heartbeat-based live presence tracking. It also respects Do Not Track.
At a high level, EU Stats gives you:
- A Spring Boot backend that receives tracking payloads and stores them in PostgreSQL
- A React frontend for managing sites and viewing analytics
- A tiny tracker script served by the backend at
/s.js?id=<SITE_ID> - Scheduled backend jobs that build aggregated reporting tables from raw pageview rows
This means you can install one script on your page, open the dashboard, and see traffic trends without needing an external analytics provider.
The default tracker currently sends:
- Site identifier
- Page path
- Page title
- Referrer domain
- User agent string
- Event type
- Event name
- Timestamp
The built-in tracker currently sends:
- Pageviews
- Heartbeats for live presence tracking
- Generic
link_clickevents
The backend also enriches requests with hashed visitor identity, device data, and country lookups before storing raw events.
The project currently takes a deliberately reduced-data approach:
- No cookies
- No
localStorage - Uses
sessionStorageonly for a per-tab live presence identifier - No raw internet protocol persistence in application logic
- Visitor identity is derived from a daily-rotated server-side hash
- Path-only page address storage
- Referrer stored as domain only
- Do Not Track respected in the tracker
Important:
- This is a technical privacy posture, not a legal certification
- You still need your own privacy notice on the tracked site
- You should still review reverse proxy, web server, hosting, and database logs separately
The flow is simple:
- You create a site in the frontend.
- The backend gives you a snippet like:
<script src="http://localhost:8080/s.js?id=1"></script>- You place that script on your website.
- When the page loads, the tracker sends a pageview payload to
POST /api/c. - While the tab stays visible, the tracker also sends heartbeat events and uses a per-tab
sessionStoragekey so the backend can estimate live presence. - The backend normalizes and enriches incoming rows, then stores them in PostgreSQL.
- Scheduled jobs aggregate those raw rows into reporting tables.
- The frontend reads the aggregated data through
/api/sites/{siteId}/d/*.
This split keeps collection simple and pushes heavier reporting work into scheduled backend processing.
- Java 21
- Spring Boot 3.4.2
- Spring Web
- Spring Data JPA
- Spring Validation
- Spring Actuator
- Liquibase
- PostgreSQL JDBC driver
- React 19
- TypeScript 5.9
- Vite 7
- Tailwind CSS 4
- Axios
- React Router
- PostgreSQL
- TypeScript source in
frontend/tracker/tracker.ts - Compiled into
backend/src/main/resources/static/tracker.js
backend/Spring Boot application, REST endpoints, ingestion, aggregation jobs, and tracker deliveryfrontend/React application for site management and analytics pagesfrontend/tracker/Tracker source code that compiles into the backend static assetdocker-compose.ymlLocal PostgreSQL service for developmentbackend/src/main/resources/db/Liquibase changelog and schema bootstrap SQL
You need:
- Java 21
- Maven
- Node.js and npm
- PostgreSQL
Docker is optional, but it is the easiest way to get a local PostgreSQL instance running.
EU Stats needs PostgreSQL.
Default local values are:
- Database:
analytics - User:
eu-stats - Password:
eu-stats - Host port:
5430 - Container port:
5432
If Docker is running on your machine, you can create the database service directly from the repository root:
docker compose up -d postgresWhat this does:
- Pulls the configured PostgreSQL image if it is not present
- Starts a container named
eu-stats-postgres - Creates the database using the values from
docker-compose.yml - Exposes the database on
localhost:5430 - Persists data in the Docker volume
pgdata
If you want to reset the local database completely:
docker compose down -v
docker compose up -d postgresYou can use any PostgreSQL instance you already have, as long as you provide matching connection settings through environment variables.
The backend uses Liquibase on startup.
That means:
- The database itself must exist before the backend starts
- Liquibase creates the tables and indexes
- The bootstrap SQL in
backend/src/main/resources/db/migration/create_fresh_schema.sqlinitializes the schema structure
The pageviews table is defined as a range-partitioned table on viewed_at.
Right now, the schema includes:
pageviewsas the partitioned parent tablepageviews_defaultas the default partition
Important:
- Dedicated month-by-month partitions are not created automatically at the moment
- If you want one table per month, a partition for the upcoming month should be created ahead of time
- That creation should ideally be automated with a scheduled database job, a Liquibase change process, or an operational script
So, for now:
- The project works with the default partition
- Monthly partition creation is a future operational improvement, not something the current code automates for you
The backend reads configuration from:
backend/.env.envin the repository root
Example backend configuration:
DB_HOST=localhost
DB_PORT=5430
DB_NAME=analytics
DB_USERNAME=eu-stats
DB_PASSWORD=eu-stats
APP_TRACKER_BASE_URL=http://localhost:8080
APP_DATA_RETENTION_MONTHS=24
APP_AGGREGATION_LOOKBACK_DAYS=2
APP_CORS_ORIGINS=http://localhost:5173,http://localhostFor the frontend, you only need an override file if the backend is not available at the default location:
VITE_API_BASE_URL=http://localhost:8080/apiFrom the repository root:
npm installThis installs the root-level helper dependency and the frontend workspace dependencies used by the root build scripts.
If Docker is running:
docker compose up -d postgresFrom the repository root:
npm run build
npm run allWhat npm run all does:
- Starts the frontend Vite dev server
- Starts the backend Spring Boot application
- Runs both processes at the same time in one terminal using
concurrently
Under the hood it runs:
npm run dev:frontendnpm run dev:backend
- Frontend:
http://localhost:5173 - Backend:
http://localhost:8080 - Health:
http://localhost:8080/actuator/health
- Open the dashboard
- Add a site
- Copy the generated tracker snippet
- Place it on your page
After the page receives traffic, open the analytics page for that site in the dashboard.
These commands are intended to be run from the repository root.
Starts the frontend and backend together for development.
Use this when you want the full project running locally in one terminal.
Builds the whole project in the right order:
- Rebuilds the tracker asset
- Builds the frontend production bundle
- Builds the backend package
Under the hood it runs:
npm run build:frontendnpm run build:backend
npm run build:frontend rebuilds backend/src/main/resources/static/tracker.js from frontend/tracker/tracker.ts before the Vite production bundle is created.
Important:
- The backend build uses
mvn -B clean package -Dmaven.test.skip=true - That means test execution and test compilation are both skipped in the root build command
npm run build:trackerRebuildsbackend/src/main/resources/static/tracker.jsfromfrontend/tracker/tracker.tsnpm run build:frontendRebuilds the tracker asset and then builds the React appnpm run build:backendBuilds the Spring Boot application jar and skips test compilation and test executionnpm run dev:frontendStarts only the frontend dev servernpm run dev:backendStarts only the backend
cd backend
mvn spring-boot:run
mvn test
mvn clean packagecd frontend
npm run dev
npm run build
npm run previewnpm run build in frontend/ also regenerates ../backend/src/main/resources/static/tracker.js.
cd frontend
npm run build:trackerGET /api/sitesPOST /api/sitesGET /api/sites/{siteId}PUT /api/sites/{siteId}DELETE /api/sites/{siteId}GET /api/sites/{siteId}/snippet
GET /s.js?id=<SITE_ID>POST /api/c
GET /api/sites/{siteId}/d/summaryGET /api/sites/{siteId}/d/contentGET /api/sites/{siteId}/d/sourcesGET /api/sites/{siteId}/d/actions
Runs on a short interval and refreshes reporting tables from recent raw pageview rows.
Deletes raw pageview rows older than the configured retention window.
The current project is intentionally smaller than a full analytics suite.
What is included:
- Pageview collection
- Top page reporting
- Referrer reporting
- Event reporting support
- Site management
What is not currently part of the default experience:
- Cookie banners
- Browser-storage-based identity
- Real-time visitor presence widgets
- Device and operating system charts
- Geographic charts
- The README describes the current codebase as it exists now
- If you later reintroduce richer analytics dimensions, update both the tracker and the README together
- If you want production use, you should still add deployment notes for reverse proxy, HTTPS, backups, and privacy policy handling