The lead ledger was one file on one PC, and a missing file read as zero leads. The file was .runs/blog-leads/leads.jsonl, and if it wasn’t there, read_leads returned []. The module’s docstring said its sources were verified on 2026-09-30. Ten days later a dependency audit flagged the ledger as row D6, and we moved it off that PC.

What the ledger holds

Each row is {slug, kind, at}, with no personal data in it. The site stamps post_slug on every intake form. The form provider delivers submissions to email and exposes no API, so rows are recorded with blog-funnel --record-lead SLUG --kind review|discovery|contact. The metrics report reads the file to count leads per post.

The old read_leads opened with if not ledger.exists(): return []. The existing test for missing sources asserts the text “no leads recorded” for that case. A missing ledger and a ledger with no leads came back as the same [].

Moving it to a Worker

The ledger now lives in a Cloudflare Worker, dxdev-leads, backed by a SQLite Durable Object on our own Cloudflare account. The commit touched 3 files, with 106 insertions and 6 deletions, 49 of those insertions in tests.

In funnel.py, the ledger constant changed:

LEADS_LEDGER = lead_metrics.RemoteLedger()

RemoteLedger in lead_metrics.py is a thin client over urllib.request. read_rows() is a GET and append(row) is a POST. Each request carries an Authorization: Bearer header, and urlopen is called with timeout=20. The token comes from secrets.get("DXDEV_LEADS_TOKEN") unless one is passed in. A runbook for the Worker is referenced in the module docstring.

read_leads and record_lead now take either kind of ledger. Given a RemoteLedger, they go to the Worker. Given a plain Path, they behave as before, which keeps the local jsonl ledger usable for tests and offline runs. read_leads filters the remote rows to ones that have a slug and a kind in LEAD_KINDS. record_lead returns only slug, kind and at from what the Worker saved.

Failures raise

The class docstring states the rule: “Failures raise: a lead read that cannot reach the ledger must never look like ‘no leads’ (the callers already surface exceptions as an error, not a zero).”

I kept the local path as it was, so the [] on a missing file still exists there. The remote path has no such branch. The docstring also says the PC is only a client of the ledger now.

A missing leads.jsonl still returns [], and a RemoteLedger call that fails raises instead.