Our sites count their visitors and watch for vulnerability scans without any outside analytics service. The logs and the dashboard run on the same machine that serves the pages, and nothing about a visitor leaves it. Built on 2026-10-09.
An outside analytics service would see every visitor before we did, and we couldn't take the history with us if we left. Our rule is open, self-hosted and swappable (see own your stack), so we built the small part we need on the observability stack we already run: OpenTelemetry, Loki and Grafana (observability).
Two kinds of line, both one JSON object written by the site server to the system journal, which passes them to Loki. Loki keeps them for 30 days.
| Line | When | What it holds |
|---|---|---|
request | Every request from outside | Site, page, status, response time, IP address, country, User-Agent, referring site, the kind of traffic (search, AI assistant, social, bot...), browser, operating system and device type, and whether it looks like a scan. |
visit | Each page view in a browser that runs JavaScript | Screen and window size, pixel ratio, time zone, languages, CPU cores and memory class, touch support, graphics card (WebGL), a hash of how the browser draws a test image (canvas), a browser fingerprint, and a session id that lasts until the tab closes. |
The visit line comes from a small first-party script, /_jayai/v.js, that every page
loads. It sends its report back to the same site and nowhere else. The server keeps only the fields it expects, as
short values, so a forged report can't flood the log. The country comes from ngrok, which already sees each
connection, so there's no IP database to keep up to date.
Static sites have no WordPress, no .env file and no admin panel, so anyone asking for one is
scanning. Each request is checked against a short list of probe patterns and known scanner tools:
.env, cloud keys, SSH keys),
version control (.git), server config, admin panels, shells and CGI, backups and dumps, path
traversal, and any other PHP.Detection only labels requests; it doesn't block them. Blocking can come later, at the ngrok edge, once the dashboard shows which IPs are worth it.
Grafana's "Site visitors and scans" dashboard reads both kinds of line from Loki, for one site or all of them:
Grafana sits behind a Google sign-in limited to us, so IP addresses and fingerprints are never public.
Every page says, in its footer, that visits are logged on our own server, what is logged, and that we never share or sell it. Fingerprints are good at telling browsers apart but not perfect: identical phones can share one, and a browser that blocks JavaScript sends none. We use the data to understand traffic and to spot attacks, and it expires after 30 days.
static_www/lib/access_log.rb: the request line.static_www/lib/scan_probe.rb: probe paths and scanner tools. Add one by adding a line.static_www/lib/visitor.js and visitor_beacon.rb: the visit script and the endpoint that
logs it.static_www/edge-rules.yml: the ngrok rule that adds the country.observability/config/grafana/dashboards/site-visitors.json: the dashboard.Each piece has Minitest tests, and the service is registered in observability/services.yml like
everything else we run (see the handoff README).
Provided as is, for information only, with no warranty and no liability. Not legal or professional advice. Security and radio material is for authorized, lawful use only. Full disclaimer.
Visits are logged on our own server: IP address, browser and device details, and pages viewed. We use them to understand traffic and spot attacks, and never share or sell them.