Public: anyone can read this; the crew can change it.

Settings → Billing shows the selling funnel for the last 7 days: visits to ho… #11263

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

Settings → Billing now opens with one funnel line for the last 7 days, and `GET /api/v1/admin/billing/funnel?days=7` returns the same numbers. The new `test/funnel.test.ts` covers the visitor path and the row, and passes. `tsc`, `gate.ts`, `design-lint` and `hollow` are clean, and so are the related suites I ran. The row's one-line fit at 1280 px is not verified: I never rendered the page, and the…

shipped production is serving it deploy 9328ecc

opened this

Settings → Billing now opens with one funnel line for the last 7 days, and GET /api/v1/admin/billing/funnel?days=7 returns the same numbers. The new test/funnel.test.ts covers the visitor path and the row, and passes. tsc, gate.ts, design-lint and hollow are clean, and so are the related suites I ran. The row's one-line fit at 1280 px is not verified: I never rendered the page, and the test only checks the row is a single line of text. I did not run the full suite or the check's skipped socket suites.

The line reads "N visits → N saw plans % → N clicked % (by plan) → N forges % → N paid %". Each percentage is a share of the step before. Under it, the top 5 referrers of visits that pressed a plan, with "(direct)" when there is none.

  • Counting: steps 1–4 are stored as a funnel scope in traffic_view. That gives them the traffic view's visitor hash, scraper flag, daily caps and 90-day retention, with no new table. Visits, plans seen and clicks count a visitor once a day, forges count forges, and scrapers are never counted.
  • Paid: read from entitlement_change, counting changes to active on a forge whose provider isn't none. Nothing in the repo writes a provider entitlement yet, so it reads 0 until the Stripe webhook lands.
  • Capture:

- The home records a visit server-side for an anonymous GET of /. - A small script on the home posts "plans in view" and plan clicks to POST /_funnel. It is same-origin and rate-limited, stores nothing under Do-Not-Track, and refuses any plan the forge doesn't offer. - Pay links carry data-plan; the "Get a forge" button counts as the free plan. - The new-forge form records a forge made.

  • Probe: GET /api/v1/admin/traffic/probe?scope=funnel adds a recomputed-in-SQL figure for each step.
  • Tests: a visitor moves home, plans, click and forge by one each, and a scraper moves none. A provider activation counts as paid and a hand grant doesn't. Referrers, the Billing row, the API, the probe and bad input are covered too.
  • Housekeeping: docs/features/selling-funnel.md has the how-to, and the new OpenAPI route entry and index are in.

Not done: forges made through the API or MCP aren't counted, only the form. Plan clicks made with scripts off aren't seen. I added no terminal screen, because there's no new page.

Try: open /settings/billing as an admin, or GET /api/v1/admin/billing/funnel?days=7.

Closes #11259

3 earlier comments · 1 event
commented

Settings → Billing now opens with one funnel line for the last 7 days, and GET /api/v1/admin/billing/funnel?days=7 returns the same numbers. The new test/funnel.test.ts covers the visitor path and the row, and passes. tsc, gate.ts, design-lint and hollow are clean, and so are the related suites I ran. The row's one-line fit at 1280 px is not verified: I never rendered the page, and the test only checks the row is a single line of text. I did not run the full suite or the check's skipped socket suites.

The line reads "N visits → N saw plans % → N clicked % (by plan) → N forges % → N paid %". Each percentage is a share of the step before. Under it, the top 5 referrers of visits that pressed a plan, with "(direct)" when there is none.

  • Counting: steps 1–4 are stored as a funnel scope in traffic_view. That gives them the traffic view's visitor hash, scraper flag, daily caps and 90-day retention, with no new table. Visits, plans seen and clicks count a visitor once a day, forges count forges, and scrapers are never counted.
  • Paid: read from entitlement_change, counting changes to active on a forge whose provider isn't none. Nothing in the repo writes a provider entitlement yet, so it reads 0 until the Stripe webhook lands.
  • Capture:

- The home records a visit server-side for an anonymous GET of /. - A small script on the home posts "plans in view" and plan clicks to POST /_funnel. It is same-origin and rate-limited, stores nothing under Do-Not-Track, and refuses any plan the forge doesn't offer. - Pay links carry data-plan; the "Get a forge" button counts as the free plan. - The new-forge form records a forge made.

  • Probe: GET /api/v1/admin/traffic/probe?scope=funnel adds a recomputed-in-SQL figure for each step.
  • Tests: a visitor moves home, plans, click and forge by one each, and a scraper moves none. A provider activation counts as paid and a hand grant doesn't. Referrers, the Billing row, the API, the probe and bad input are covered too.
  • Housekeeping: docs/features/selling-funnel.md has the how-to, and the new OpenAPI route entry and index are in.

Not done: forges made through the API or MCP aren't counted, only the form. Plan clicks made with scripts off aren't seen. I added no terminal screen, because there's no new page.

Try: open /settings/billing as an admin, or GET /api/v1/admin/billing/funnel?days=7.

commented

Settings → Billing now shows the selling funnel for 7 days, and the failing check is fixed. I ran test/forge-looks.test.ts and bun x tsc --noEmit, both clean, then pushed. I did not re-run the full suite or the gate; the forge's check does that on this push.

What failed: the check failed because POST /_funnel, the home page's beacon route, was not on the not-a-page list in test/forge-looks.test.ts. That test requires every top-level route to be either a picture or listed there with a reason. I added /_funnel to the list as a POST-only mutation, covered by test/funnel.test.ts. The test was out of date, so I changed the test and not the code.

What the pull does

  • Billing: Settings → Billing shows home visits, plans seen, plan clicks by plan, forges made and paid.
  • API and probe: GET /api/v1/admin/billing/funnel and the traffic probe (scope=funnel) return the same numbers.

To try it: open /settings/billing as an admin, or GET /api/v1/admin/billing/funnel?days=7.

Changes requested — before the latest push

Changes needed (no security finding is high; the misses are the terminal door, the design, and the direction).

  1. Both doors: Settings → Billing gains the funnel but there is no screen in src/tui/, so a person in the terminal cannot see it. Add the screen with its entry where AGENTS.md says, read-gated to admins like the page, and name it in docs/features/selling-funnel.md. "No new page" is not an answer: the page's new content is a new thing to see.
  2. Design (DESIGN.md 1, 2, 7, 8): see the line comments. The funnel is a run-on sentence of arrows; make it rows.
  3. Direction: the owner put the pipeline items (#6534, #6611, #5276, #3316, #6217, #6428, #1242, #1073) before any feature, and this is a feature. Say in the pull which pipeline item it serves, or hold it behind them.
  4. The one-line fit at 1280px was never rendered (the builder says so); the rows fix removes that risk, and the preview must show it working.

Security read: /_funnel checks origin, DNT, a per-client rate keyed on the socket, a 200-byte bounded read, closed field set and a known plan; the API is admin-gated. Fine.

commented

Settings → Billing now shows the funnel as rows, and there is a terminal screen for it. I ran tsc, the gate, the design lint and 83 tests across the funnel, billing, palette and both-doors suites, all passing. I have not looked at the preview, so the 1280px render is not checked, and the full suite was not run here (the changed files reach 1,875 test files, too many to run locally); the forge's check runs it on this push.

  • Terminal door: the palette entry "From visit to paid" (src/tui/screens/funnel.ts) is offered to a forge admin who is a person, the same people who see the page. It shows the same rows, and w flips 7 and 30 days. It is added to ENTRIES and named in docs/features/selling-funnel.md. test/tui-funnel.test.ts checks that an admin reads every step and a member is offered nothing.
  • Design: the heading is now "Last 7 days: from visit to paid", and the block sits below Provider and Plans. Each step is a dl row with its count and "N% of the step before". Clicks by plan and plan-click referrers are rows under it. The arrow sentence is gone, so there is no one-line fit to break at 1280px or on a phone. I updated test/funnel.test.ts for the new markup and the position.
  • Direction: I can't name a pipeline item (#6534, #6611, #5276, #3316, #6217, #6428, #1242, #1073) this serves. It serves the owner's own request in #11259, which asked for the selling-funnel numbers because the home now sells and its traffic was unmeasured. The latest commit on the branch (767200b16) records that the pipeline items do not block it. It is a feature ahead of the pipeline, and whether to hold it behind them is the owner's call.
commented

The check failed because the admin palette roster test didn't list the new "From visit to paid" screen, and I've fixed that and pushed.

I added the missing entry to the expected admin roster in test/palette-roster.test.ts, just before "Who can do what". The test was out of date, so the code is unchanged. The new entry is the terminal screen added earlier on this pull, and it is shown to admins only.

test/palette-roster.test.ts now passes (15 tests) and bun x tsc --noEmit is clean. I did not re-run the full suite or bun scripts/gate.ts here, so the forge's check on this push is the full run.

Approved

Earlier asks are met: the Billing funnel is now dl rows (principles 3, 6 and 7), there is a terminal screen gated to person admins with its how-to in docs/features/selling-funnel.md, README is untouched, and the tests cover the new behaviour. I could not diff against 84d2d6375224 because the commit is not in my clone, so I read the whole change. The beacon is same-origin, capped at 200 bytes by bytes read, rate-limited by socket bucket, honours Do-Not-Track, and only stores plans the forge offers. The API and probe are admin-gated. Non-blocking: paidSql is called by both the report and the probe, so the probe's funnel_paid agreement is trivially true; recount it independently. Non-blocking: this is a feature ahead of the pipeline-first list, but it is the owner's own #11259 request, so it lands.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs