Self-hosted, cookie-free web analytics in a single Rust binary.
No database. No cookies. No JavaScript bundle on the client — just a 1×1 pixel, an nginx log, and a dashboard that answers "who visited my site, from where, and why?"
Google Analytics is a 400 KB tracker that feeds your visitors' data to an ad company. FRKN Pixel is the opposite:
- Privacy-first — a transparent 1×1 GIF request. No cookies, no fingerprints, no personal data, GDPR-friendly by design.
- You own the data — everything lives in one snapshot file on your server. Nothing leaves your infrastructure.
- Absurdly light — a ~2 MB static Rust binary with zero runtime dependencies. No ClickHouse, no Postgres, no Redis.
- Free geo — visitor countries resolved locally from a GeoLite2 database.
- Ops-friendly — Prometheus
/metricsendpoint out of the box, systemd units, musl static builds.
Time ranges: Today · Yesterday · 24H · 7D · 30D · 3M — switchable in one click, shareable via URL hash.
| 7 days, all tables | 3 months trend |
|---|---|
The dashboard tracks:
- Visits over time — interactive chart with hover tooltips
- Per-host analytics — filter the whole dashboard by host (
frkn.org,labs.frkn.org, …) - Top pages / hosts — what content actually gets read
- Top countries — GeoIP-resolved visitor geography
- Top sources & campaigns — full
utm_source/utm_medium/utm_campaignattribution - Top referrers — who sends you traffic
Running several sites or environments through one pixel? Pick a host in the
header and every card, chart and table shows only that host's data — switch
back to All hosts for the global view. The selection lives in the URL hash
(#7d/labs.frkn.org), so filtered views are shareable links.
Hosts are listed via config — nothing is hardcoded:
dashboard_hosts = ["frkn.org", "labs.frkn.org", "beta.frkn.org"]Leave it empty and the list is built automatically from the hosts seen in the data.
flowchart LR
A[Visitor's browser] -->|1×1 pixel request| B[nginx]
B -->|access log| C[pixel-agent]
C -->|GeoLite2| C
C --> D[Admin dashboard]
C --> E[Prometheus /metrics]
- Your page loads a tracking pixel:
GET /pixel?page=/pricing&host=example.com&ref=...&lang=en&utm_source=telegram - nginx writes the request to its access log — that's the only "ingestion pipeline".
pixel-agenttails the log, parses each hit, resolves the country via GeoIP and aggregates everything into 5-minute buckets.- The built-in dashboard and Prometheus endpoint serve the aggregated metrics. State is persisted to a compact rkyv snapshot, so restarts are instant and lossless.
Because the source of truth is a plain nginx log, you can rebuild the entire history at any time with the backfill tool — see BACKFILL_README.md.
<img src="https://media.example.com/pixel?page=/pricing&host=example.com&lang=en"
width="1" height="1" alt="" style="position:absolute;left:-9999px">UTM parameters from the page URL are forwarded automatically when you use the
helper script — see dev/analytics.js for a production example.
# 1. Build (or grab a static musl binary from Releases)
cargo build --release --bin pixel-agent
# 2. Configure
sudo mkdir -p /etc/pixel-agent
sudo cp pixel-agent-example.toml /etc/pixel-agent/config.toml
sudo $EDITOR /etc/pixel-agent/config.toml
# 3. GeoIP database
sudo mkdir -p /usr/share/GeoIP
curl -fsSL -o /usr/share/GeoIP/GeoLite2-Country.mmdb \
https://raw.githubusercontent.com/adysec/IP_database/main/geolite/GeoLite2-Country.mmdb
# 4. Run
sudo cp target/release/pixel-agent /usr/local/bin/
sudo cp pixel-agent.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now pixel-agentThe dashboard is now on http://your-server:9102/ — put it behind nginx with
auth (see docs/nginx.conf) and you're done.
One-command deploy of a released version:
sudo ./deploy/pixel-agent-deploy.sh v0.2.0All options with defaults — see pixel-agent-example.toml:
| Option | Default | Description |
|---|---|---|
log_path |
— | nginx pixel log to tail |
geoip_db |
— | path to GeoLite2 Country .mmdb |
admin_listen / admin_port |
0.0.0.0 / 9102 |
dashboard & API bind address |
bucket_minutes |
5 |
aggregation granularity |
retention_hours |
2160 (90 days) |
how long aggregated history is kept |
max_points |
30000 |
max data points per metric series |
snapshot_path |
/var/lib/pixel-agent/metrics.snapshot |
persistent state file |
poll_interval_sec |
30 |
log tail interval |
cors_origins |
["http://localhost:8080"] |
allowed API origins |
dashboard_hosts |
[] (auto from data) |
hosts shown in the dashboard host filter |
Note: ranges up to 3 months need the matching retention — the defaults above already cover it. If you lower
retention_hours, long ranges in the UI will simply show less history.
| Endpoint | Description |
|---|---|
GET / |
admin dashboard |
GET /api/metrics?from_ms=…&to_ms=… |
raw time-series JSON for any range |
GET /api/config |
dashboard UI config (host filter list) |
GET /metrics |
Prometheus exposition (last 24h sums) |
GET /health |
health check |
Rebuilding stats from rotated logs (plain or .gz):
pixel-agent-backfill /etc/pixel-agent/backfill.toml /var/log/nginx/pixel.log.1.gzDetails and the periodic systemd timer: BACKFILL_README.md.
Rust 2021 · warp · tokio · dashmap · rkyv · nom · maxminddb · chrono — and a single dependency-free HTML file for the dashboard, embedded into the binary.