frameworktracking-doctrine · v31099:cursor-joseph-os2026-09-01served from databaseAll documents

Tracking doctrine — DB events, /insights per host, real people only

Tracking doctrine — store it here, show it there, tell every AI this

BLUF. Every public host writes events into our [Database] and serves https://<that-host>/insights for that hostname only. Map and phone taps are first-class events, wrapped so the server sees them. Count real people only. Do not pay an analytics company for “top tier.” Pay only if you need completed calls, which a click is not. This is directive v64 rule 6 / R13.

Live: https://projects.jbnx.io/framework/tracking-doctrine Customer report: https://projects.jbnx.io/framework/monthly-funnel-proof


What you say to any AI (paste this)

Copy the block. After the first time, put the same text in AGENTS.md / CLAUDE.md as a pointer (this URL), not a second bible. Long process stays here; always-apply files stay short.

TRACKING — mandatory on every public site we touch. Spec:
https://projects.jbnx.io/framework/tracking-doctrine
https://projects.jbnx.io/framework/monthly-funnel-proof

1. Source of truth is OUR [Database], never a third-party dashboard.
   Do not add Google Analytics, Tag Manager, Clarity, Meta Pixel,
   PostHog, Mixpanel, or any collector as the number we report.
   Optional internal tools are allowed only if events still land
   in our DB first.

2. Every public hostname must already have, or you must add:
   a) Server/edge log of each real document request → `analytics.hits`
      (host, path, status, referrer_host, utm_source/medium/campaign/
      content/term, click ids, sid). Set sid as HttpOnly cookie on
      OUR domain (Lax, 90 days). Hash or drop raw IP; keep country.
   b) Same-origin POST /collect (or sendBeacon) → `analytics.events`
      for in-page actions. No third-party host.
   c) tel: and maps/directions links go through first-party wrappers
      /go/call and /go/map (then 302). Server writes the click only
      when it is a real user navigation — not curl, not prefetch.
   d) On signup, booking, payment, lead: copy sid + utms onto THAT
      row in the product database. Finished outcomes are those rows.

3. Required event names (no extras without a named funnel stage):
   page_view, cta_click, outbound, click_to_call, click_to_map,
   form_start, form_finish. Never send form field values, names,
   emails, or phone numbers in the event payload.
   One page_view per document request. Do not also fire a JS
   page_view for the same load.

4. Every public host serves GET /insights showing ONLY that Host's
   rows (e.g. marcobros.localfave.org/insights). Monthly snapshots
   may also render in the billing login. Same tables. No third-party
   dashboard as the page. The page must say bots, local testers,
   and Insights-page views are dropped.

5. We claim only tagged landings (our UTM or click-id). Direct and
   unknown stay in an “unattributed” column. No modeled conversions.

6. Count only real people. Drop, and never show:
   - bots, empty UA, crawlers, curl/wget/python/node-fetch
   - local/private IPs and localhost referrers
   - /insights, /collect, /api/*, /go/*, robots, health, *-probe
   - prefetch / prerender / HEAD
   - test labels: verify, prodit, done, x
   - /go/call and /go/map that are not a user navigation
     (sec-fetch-user: ?1, or dest=document + mode=navigate,
     or no sec-fetch headers on an old browser)

7. If a host ships without a–d, the job is not done. Verify:
   GET /insights → 200, that host only, and that GET does not
   increment page views. A scripted GET /go/call still 302s and
   must NOT increment the call number. A bot POST /collect
   returns {ignored:true} (or no new row).
   Do not curl /go/call as proof — that is how we faked calls.

That is the whole instruction. If they ignore it, the live URL is the argument, not a longer paste.


Count only real people

This is the quality rule. A number on /insights is a lie if it includes us, bots, or the page checking itself.

Drop on write (do not insert) and drop on read (Insights SQL / page must filter the same list):

DropHow you know
Bot / crawler / scriptEmpty UA, or UA matching bot/spider/crawl/curl/wget/python/node-fetch/healthcheck/headless. Do not treat the Worker→portal hop UA (Cloudflare-Workers) as the visitor — use body.ua.
Local tester127.0.0.1, localhost, RFC1918, ip_kind=local, local referrer
Ops / this pagepage_view on /insights, /collect, /api/, /go/, robots, sitemap, favicon, /healthz, *-probe (including /post-worker-probe)
PrefetchPurpose: prefetch, prerender, HEAD
Fake tap/go/call or /go/map without a user navigation. Curl and link prefetch still 302. They do not increment.
Test labelverify, prodit, done, x (and test when it is our verify string, not a UI place name like header / page)

place=header and place=page are real UI. They are not test labels.

Verify without poisoning the report


All stats in our database

One schema, append-only. Suggested names (do not invent a second):

TableWhat landsWho writes it
analytics.hitsEvery real document requestEdge / app middleware
analytics.eventsClicks, SPA views, /go/* wrappersSame-origin /collect or the wrapper
Product tablesFinished outcomes (booking, user, payment, lead)The product, with sid / utm columns
analytics.month_snapshotsFrozen month per productJob on the 1st

Do not store: raw IP, full user-agent forever, form fields, session replay, staff passwords. Country and a daily-rotated hash are enough for “uniques” if you ever need them internally. They are not a customer headline.

The existing POST /api/1099/visit-beacon is geo only for #/map. It does not replace hits / events. Keep it as a coarse map feed if you want; wire the real funnel to the tables above.


Where to show the stats

Collect and show on that domain. Directive v64: every public host we ship has /insights scoped to that Host, counting real people only.

SurfaceURL shapeAudience
Collector (invisible)https://<host>/collect and /go/call /go/mapBrowser only
Live insightshttps://<host>/insights — this hostname onlyOwner / us / customer
Examplehttps://marcobros.localfave.org/insightsMarco Bros rows only
Customer monthly proofBilling login freeze (same tables, month snapshot)Paying customer
Internal deskportal #/map (geo only, not the funnel)Us

Sibling subdomains never share a page. other.localfave.org/insights must not see Marco Bros. Do not invent stats. as a second host — /insights on the real host is the address.

Filter every query by host = request.hostname. That is the whole multi-tenant rule.

Every Insights page prints the quality line: bots, local testers, this page, prefetch, and scripted /go/* taps are dropped.


Map clicks and phone clicks

These are the local-business funnel (nail booking, “call us,” “directions”). Treat them as stage-2 events, not as finished customers.

Phone

Maps / directions

Why wrap instead of only JavaScript

Ad blockers and in-app browsers eat listeners. A first-party /go/* link is a normal document request. Also put data- + beacon on the same link for SPA pages. Two writes, same sid; dedupe on (sid, name, minute).

Never put a raw tel: or maps.google.com in the header without the wrapper.

Do not verify by curling the wrapper. That is how agents inflated call counts. The redirect working is not the same as a customer tap.


Is this the best we can do?

For trustworthy monthly proof stored in our database: yes. This is the top of the stack that still belongs to you.

What would be “more” and is not better for this goal:

TemptationWhat you getWhy we skip it
Google Analytics 360, Adobe, Mixpanel paidPrettier attribution UIData not ours; customer cannot audit; modeled gaps
Clarity / FullStory / HotjarRecordings, heatmapsNot an outcome; PII risk; not on the invoice
Device fingerprintingStickier uniquesHostile; consent; we do not need it
Public per-domain dashboardsLooks like a productTwo numbers; leaks; support burden

The honest ceiling above this doctrine is identity you already have: they logged in, they paid, they booked. That is already in the product tables. Tracking does not get better than joining sid to that row.


Do we pay anyone for top-tier stats?

No analytics vendor. Not Google, Microsoft, PostHog Cloud, Mixpanel, Amplitude, Adobe, Contentsquare. Paying them buys their UI and their lock-in. It does not make the customer report more true.

The one paid thing that can be worth it: completed-call tracking, and only if a product’s North Star is the phone.

Ads platforms (search/social) get conversion pings from our server only when we are buying ads and the customer asked for that. Those pings are copies. The booking row remains the original.


Done means

A site is tracked when, on the live host:

  1. A normal visit inserts analytics.hits. A curl does not.
  2. Tapping Call inserts click_to_call via /go/call. A scripted GET does not.
  3. Tapping Map/Directions inserts click_to_map via /go/map. Prefetch does not.
  4. A finished booking/signup/payment row stores sid and utms.
  5. GET /insights is 200, that host only, and does not increment page views.
  6. No third-party analytics script is required for those to be true.

One live check each. No second agent to re-verify. Do not curl /go/call as the check.