Tally

A visitor tracker and stats engine in core Perl. It counted your arrival on this page before the HTML reached you.

Open the live dashboard →

Three ways to capture

  1. 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.

  1. Client beacon/tally/tally.js posts screen size, viewport, timezone

and document title to track.cgi, which also answers as a 1×1 pixel for no-JS clients.

  1. Access-log importtally import /var/log/nginx/access.log replays

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:

never written to disk.

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};