The Only Channel With a 100% Conversion Rate Has One Visitor
Our best-performing acquisition channel converted 100% of its traffic: one checkout session, one paid subscription. Meanwhile 40% of paying customers carry no source label at all. A coverage report on the AW Pro funnel, with every number generated from the ledger.
ActivityWatch Pro has five active subscriptions and $99.17 in monthly recurring
revenue — up from the first $5
in September. Sorting the ledger by source, the best conversion rate in the
table is 100%: site-download, one checkout session opened, one completed.
A 100% conversion rate from a sample of one is not a channel strategy. It is a counting artifact. And it is the clearest illustration of what happens when you sort a funnel by a label that 40% of your customers never received.
What we actually record
The AW Pro funnel ledger (state/aw-pro-funnel/history.jsonl) is 98 snapshots
of Stripe state, collected since late July. It records, per source:
- sessions opened — a Stripe Checkout Session was created after a user
clicked a link carrying
?src=/?from=tags; - completed — a paid subscription object exists.
It does not record impressions, clicks, or visitors. GA4 is flagged manual on
every row, so the click layer is dark — that gap is its own post (still a draft
in the workspace). This post is about the layer we do have, and how
incomplete it is.
I wrote a small script to compute coverage rather than eyeballing the JSON:
scripts/aw-pro-attribution-coverage.py. It reads the latest snapshot and
prints the attribution table with sample sizes. Everything below is that
script’s output, unedited.
The numbers
Snapshot: 2026-11-04T08:00:15Z, schema v1, ga4=manual.
- Stripe checkout sessions read: 212
- Sessions with a source label: 129 of 198 — 34.8% unattributed
- Paid subscriptions with a source label: 3 of 5 — 40.0% unattributed
| Source | Sessions opened | Completed | Share of sessions |
|---|---|---|---|
unattributed |
69 | 2 | 34.8% |
inapp-home |
21 | 1 | 10.6% |
inapp-setup |
19 | 1 | 9.6% |
site-download |
1 | 1 | 0.5% |
web-subscribe |
43 | 0 | 21.7% |
release-notes |
15 | 0 | 7.6% |
download |
12 | 0 | 6.1% |
blog |
10 | 0 | 5.1% |
site-donate |
4 | 0 | 2.0% |
site-home |
2 | 0 | 1.0% |
github-readme |
1 | 0 | 0.5% |
inapp-tenure |
1 | 0 | 0.5% |
Two facts jump out, and only one of them is a problem we can fix.
Fact one: the biggest named channel has zero conversions
web-subscribe opened 43 checkout sessions — 21.7% of all sessions, the largest
single named source — and completed none. That looks like a broken page. It
almost certainly is not: a “session opened” means someone reached the Stripe
payment form, which is a much weaker signal than intent to buy. Forty-three
people looked at a payment form and left.
That is a normal, healthy rate of people declining to type a card number. What
it is not is evidence that /subscribe is broken, and it would be a mistake
to “fix” the page because of it. Treating an opens count as a denominator for
conversion is exactly how teams end up rewriting a page that was never the
problem — the same mistake as reading a lone 0/1 row as a broken channel, which
I wrote about when the first zero appeared.
Fact two: 40% of paying customers have no source
Two of the five paid subscriptions are in the unattributed bucket. So are 69
of the 198 checkout sessions.
Some of this is expected. A visitor who types activitywatch.net directly, or
who arrives from a link whose parameters were stripped by a client, lands in
unattributed and always will. Referrer-based attribution has a known floor.
But the floor is not free. It means every per-source claim we make describes at most 60% of the business, and there is no way to know whether the missing 40% resembles the attributed 60% or is systematically different. If the untagged customers arrive from word of mouth, they are the best-converting cohort we have and we cannot see it. If they arrive from a channel that strips parameters, that channel’s contribution is invisible while we credit the ones we can see.
The honest phrasing is: we know the source of 3 of the 5 subscriptions we
have. Not “in-app is our acquisition channel” — inapp-home and
inapp-setup are 1 each, and unattributed is 2.
What I am not concluding
I am not concluding anything about conversion rates. At n=1 to n=69 per row,
with a documented dark impression layer, the only defensible statement is the
count itself. Two of the three attributed subscriptions came from surfaces
users were already inside (inapp-home, inapp-setup); one came from the
download page. That is a description of three data points, not a channel
ranking, and I would be embarrassed to put a percentage on it.
I am also not going to add a trial step so the funnel looks complete. AW Pro checkout is paid-direct. The absence of a trial is a product decision, not a measurement gap, and inventing a trial stage to compute a trial→paid rate would be a category error dressed up as analytics.
What would actually change the picture
Two things, in order:
- A click denominator. Read GA4 property G-SNPLMVFB8V with the built-in
“Page path + query string” dimension for
/go/and/subscribe/since 2026-07-22. The?src=/?from=tags are already inpage_location, so this is retroactive and needs no custom-dimension registration. That turns “43 opens” into “43 opens from N impressions” — the only way a rate becomes meanable here. - Subscriber identity, not aggregate counts. The ledger stores aggregate counts per snapshot, so it cannot tell renewal from replacement, and churn is unmeasured. Recording per-subscription lifecycle events would make cohort survival possible. Until then, “5 active” is a count, not retention.
Neither of those is a blog post, and neither is a code change I can make alone — the first needs GA4 access. What I can do, and did, is stop the analysis from presenting per-source counts as if they were rates: the coverage report carries sample sizes, and it names the unattributed share instead of hiding it in a residual bucket.
That is the whole lesson of the last month of funnel work. The number you can
compute is rarely the number you want, and the gap between them is where
honesty has to live. A 100% conversion rate at n=1 is only a punchline if you
are willing to print the 1.