FRD-015
Presence tracking that stores nothing worth stealing
A live view of who is on the site needs a hashed session id, a page name and two timestamps. No address, no user agent, no history.
Seeing who is on the site right now is genuinely useful for an operator — it shows whether a release broke something, and what people actually do. It is also an easy place to accumulate a surveillance log nobody asked for.
Keep one row per browser session: a hash of the session id, the user id, a page name, when the session started and when it was last seen. That supports a live roll call and nothing else.
Prune it from cron and on sign-out. There is no reason for yesterday's presence to exist.
/*
CREATE TABLE presence (
skey CHAR(64) NOT NULL PRIMARY KEY, -- sha256 of the session id, never the id
user_id INT UNSIGNED NOT NULL,
page VARCHAR(40) NOT NULL DEFAULT '',
started_at DATETIME NOT NULL,
seen_at DATETIME NOT NULL,
KEY ix_seen (seen_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
*/
function presence_touch(PDO $pdo, int $userId, string $page): void
{
if ($userId <= 0) {
return; // signed-in users only; no guest rows
}
try {
$skey = hash('sha256', session_id()); // the raw id never lands in a table
$pdo->prepare(
'INSERT INTO presence (skey, user_id, page, started_at, seen_at)
VALUES (?, ?, ?, NOW(), NOW())
ON DUPLICATE KEY UPDATE page = VALUES(page), seen_at = NOW()'
)->execute([$skey, $userId, substr($page, 0, 40)]);
} catch (Throwable $e) {
// Presence must never be able to break a page.
}
}
function presence_now(PDO $pdo, int $idleSeconds = 300): array
{
$st = $pdo->prepare(
'SELECT p.page, p.started_at, p.seen_at, u.username,
TIMESTAMPDIFF(SECOND, p.started_at, NOW()) AS session_seconds,
TIMESTAMPDIFF(SECOND, p.seen_at, NOW()) AS idle_seconds
FROM presence p JOIN users u ON u.id = p.user_id
WHERE p.seen_at > DATE_SUB(NOW(), INTERVAL ? SECOND)
ORDER BY p.seen_at DESC'
);
$st->execute([$idleSeconds]);
return $st->fetchAll(PDO::FETCH_ASSOC);
}
Using it
Poll the admin view from its own JSON endpoint every few seconds and pause when the tab is hidden. A live view that keeps polling in a background tab is a self-inflicted load problem.
Mask usernames for preview or demo admins, the same as everywhere else.
Delete rows on sign-out, and prune anything older than an hour from cron.
What bites people
Wrap every write. A missing presence table should not white-screen the site.
Do not add the address "just for debugging". The value of this design is that there is nothing in it to leak.
Excluding guests is deliberate: starting a session for every anonymous visitor creates rows describing nobody and inflates your numbers.