Tally
A visitor tracker and stats engine in core Perl. It counted your arrival on this page before the HTML reached you.
Three ways to capture
- Server-side —
$tally->record(\%ENV)from inside the app. IP, user
agent, referrer, language, method, host. This site does exactly that, in
public/app.cgi.
- Client beacon —
/tally/tally.jsposts screen size, viewport, timezone
and document title to track.cgi, which also answers as a 1×1 pixel for
no-JS clients.
- Access-log import —
tally import /var/log/nginx/access.logreplays
Apache/Nginx combined logs, skipping assets and non-2xx/3xx, synthesising a visitor and session from a hash of IP + user agent in 30-minute buckets.
What it derives
Raw request fields go in; enriched events come out. Tally::Agent classifies
browser, OS, device and bot from the user agent. Tally::Referrer turns a
referrer into a channel (direct, search, social, referral) and pulls a search
keyword when one is present.
Stats are computed on demand over a date range: pageviews, unique visitors split new/returning, sessions, bounce rate, pages per session, a per-day timeseries, and top-N tables for pages, entry pages, referrers, channels, browsers, operating systems, devices, languages, keywords and screen sizes.
Geolocation with no network calls
Tally::Geo resolves IP→country entirely offline. There is no lookup
service, no API key, and no outbound connection — a deliberate constraint.
A local packed database (data/geo.tdb) is built once from a freely
redistributable CSV:
tally geo-import dbip-country-lite.csv
The format is a 6-byte magic, a record count, then fixed-width sorted ranges — 10 bytes per IPv4 range, 34 per IPv6 — binary-searched in process. The country is resolved from the full address before anonymisation, then the raw address is discarded.
Privacy defaults
The defaults are the private ones, and they are all configurable:
- Do Not Track is honoured —
DNT: 1means the request is not recorded. - IPs are anonymised — a salted hash plus a /24 network; the raw address is
never written to disk.
- Bots are flagged and excluded from the human-facing numbers.
- Retention — this deployment prunes events after 90 days.
Storage
Append-only NDJSON, one file per day, one event per line, flock-guarded:
data/events/2026-07-25.log
No database, no schema migration, and grep still works.
Embedding it
use Tally;
my $tally = Tally->new( root => '/opt/perl-site' );
my $rec = $tally->record( \%ENV );
print "Set-Cookie: $_\r\n" for @{ $rec->{cookies} };
my $stats = $tally->stats( from => '2026-07-01', to => '2026-07-25' );
printf "%d views from %d visitors\n",
$stats->{totals}{pageviews}, $stats->{totals}{visitors};