Project

footfall

0.0
The project is in a healthy, maintained state
Footfall is what a shop counts at its door: people coming in, not who they are. Rack middleware that counts page views, visitors, the sites they came from and file downloads into a SQLite file, and a report for the terminal. No cookies, no IP addresses, no full referrers: a visitor is a hash salted anew every day and the salt forgotten the next, so a day's visitors can be counted but not followed from day to day or traced back. Bots are left out.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

Runtime

>= 2.2
>= 2.0
 Project Readme

footfall

Cookieless visitor, page view and download counts for a Rack app, in a SQLite file. Footfall is what a shop counts at its door: people coming in, not who they are.

  • Pages, visitors, referrers and downloads. A page view is a GET of HTML (or an Atom or RSS feed) answered with 200; a download, a GET under /download/ that sends the whole file, counted once even when it's resumed in pieces.
  • Nobody can be picked out. A visitor is a hash of their address and browser, salted anew every day; the salt is forgotten the next day, so a day's visitors can be counted but not followed from day to day, or traced back. No cookies, no IP addresses, and only the site a visitor came from, not the full referrer.
  • Bots left out: crawlers, link previews, monitors and command-line tools.
  • One file, no service. SQLite in WAL mode, next to your app; a report for the terminal.
  • Never breaks a page. A counting error is logged and the response goes out as it was.

Install

# Gemfile
gem "footfall"

Count

# config.ru
require "footfall"

# Outermost, so it sees every response, downloads too. Without a path it counts nothing,
# which suits development and tests.
use Footfall::Middleware, path: ENV["FOOTFALL_PATH"]

Options:

Option Default
path: (required) The SQLite file, made with its folder; nil turns counting off.
downloads: "/download/" The path prefix of files counted as downloads; nil for none.
host: the request's host This site, whose own links aren't referrers (www. is ignored).
client_ip_header: none The header a proxy puts the visitor's address in, such as Cloudflare's "CF-Connecting-IP". Only behind such a proxy: anyone can send it.
bots: Footfall::Middleware::BOTS A regexp of user agents left out.

In Rails, config.middleware.insert_before 0, Footfall::Middleware, path: ….

Report

require "footfall"

Footfall::Report.new("storage/footfall.sqlite3",
  title: "example.com",
  downloads: { "App downloads" => "%.dmg", "Plugin installs" => "Plugin-%" }
).print(days: 30)
example.com, the last 30 days (since 2026-09-10); bots left out

Totals
  Visitors (a day each)  Page views  App downloads  Plugin installs
  ---------------------  ----------  -------------  ---------------
                    412         903             57               12

By day
  Day         Visitors  Views  App downloads
  …

Then the busiest pages, the sites visitors came from, every download, and the feed's readers on the latest day. downloads: names a column in the totals per kind of file, as SQL LIKE patterns of the file names; the first is also the day by day column. Feeds (feeds:, "%/feed.xml" by default) are counted apart from pages, since readers poll them.

A small script makes it a command, for example for kamal app exec:

#!/usr/bin/env ruby
require "bundler/setup"
require "footfall"

Footfall::Report.new(ENV.fetch("FOOTFALL_PATH"), title: "example.com")
  .print(days: Integer(ARGV.first || 30))

The data

Three tables, for queries of your own:

  • views: day, path, referrer (a host, or ""), visitor, count, one row per day, page, referrer and visitor;
  • downloads: day, file, visitor, count;
  • salts: today's salt only.

Footfall only creates tables that aren't there and never alters existing ones; give it a file of its own. Days are in the server's time zone. Visitors are only comparable within a day: count DISTINCT day || visitor for "visitors, a day each".

License

MIT