Click Tracking Pipeline
smo1 intermediate 7 min read
ELI5
When someone clicks a short link they are sent on immediately; writing down the click happens afterwards in the background. The API first bumps the link’s counters in the main database (that is what the score reads), then stores the detailed click row. That row goes to ClickHouse if purr-api could reach ClickHouse when it started, otherwise to a table in the main database.
Technical Deep Dive
Source: zoomies-edge src/analytics/tracker.ts:16-77, src/handlers/redirect.ts:163-171, src/index.ts:201-231 (origin/main 5bdde59); purr-api cmd/server/main.go:116-122, 162-172, internal/handler/redirect_handler.go:51-93, 322-385, internal/service/analytics_service.go:70-160, 341-346, internal/service/click_store_postgres.go:28-40, internal/service/click_store_clickhouse.go:42-45, internal/repository/link_repository.go:148, migrations/000008_link_clicks.up.sql (bb6cfc2); smo1-io docs/decisions/2026-10-09-launch-to-revenue-scope.md:40, 57, 123, read 2026-10-25.
Pipeline
sequenceDiagram autonumber actor U as User participant Z as zoomies-edge participant API as purr-api participant PG as Postgres participant CS as ClickStore U->>Z: GET smo1.io/slug Z-->>U: 302 Location Z-)API: POST /api/analytics/track with slug, ip, ua, referer, country, city, visitor_id API->>PG: GetBySlugUnfiltered slug, uid or custom_slug API->>API: parse UA, referer, visitor hash, is_bot API->>PG: IncrementClicks on links API->>PG: RecordVisitor into link_visitors API->>PG: IncrementTotalClicks on users API->>CS: InsertClick, ClickHouse or Postgres link_clicks API-->>Z: 204 alwaysWhy async
The redirect must not wait on analytics; the code comment states a p99 target of 100 ms (redirect.ts:165). The tracking promise is registered with waitUntil so the Workers runtime does not cancel it after the response.
Counters first, store second
TrackClick increments links.total_clicks and last_clicked_at, records the visitor and increments users.total_clicks before the event insert (analytics_service.go:136-157). pawprintz reads those Postgres columns, so scores keep moving even if the event store is down. Every error on the track endpoint still returns 204 so the edge never retries or logs noise.
Which store
main.go:116-122 pings ClickHouse once at boot using CLICKHOUSE_HOST (default localhost), CLICKHOUSE_PORT, CLICKHOUSE_DB. On failure clickhouseDB is nil and NewPostgresClickStore writes to link_clicks (migration 000008), which was added for exactly this fallback by the 2026-10-09 launch scope. Production’s store is UNVERIFIED: a 2026-10-10 read-only Railway check found no ClickHouse variable in production, ClickHouse provisioning is deferred to M4.5, and the production project census on 2026-10-18 listed only Postgres and purr-api. Both facts point to Postgres; the boot log line Click analytics store: ... would confirm it.
link_clicks columns: link_id, user_id, clicked_at, visitor_id, country, city, device, browser, os, referrer_domain, utm_source … utm_content, is_bot. There is no IP column.
Gaps
- Proxy-mode clicks are never tracked.
handleProxyModestreams the destination without callingtrackClick(index.ts:201-231). - Attribution is host-blind. The track POST carries only
slug; purr-api resolves it withuid = $1 OR custom_slug = $1 LIMIT 1. Apetrova.smo1.iocustom slug that collides with an apex custom slug can be credited to the wrong link. - The edge
visitor_idis discarded; see smo1-007. - Clicks served by purr-api’s own
/:identifierroute (direct hits onpurr.smo1.io/<uid>) are tracked from Gin withCF-IPCountryas country (redirect_handler.go:75-85).
Key Terms
- ClickStore — interface with ClickHouse and Postgres implementations, chosen once at boot
- link_clicks — the Postgres fallback event table (and the ClickHouse table name in litter-box)
- IncrementClicks — the counter update scores depend on
- 204 always — the track endpoint never reports failure to the edge
Q&A
Q: Why is the redirect returned before analytics are recorded?
A: So a slow or failing purr-api, ClickHouse or Postgres never delays or breaks a visitor’s redirect. The Worker sends the 302 and keeps the tracking POST alive with waitUntil.
Q: Which database stores raw click events?
A: Whichever store purr-api chose at boot: ClickHouse if it answered the startup ping, otherwise Postgres link_clicks. Production is most likely on Postgres; that is UNVERIFIED pending the boot log.
Q: What hash function is used for visitor deduplication? A: SHA-256 in purr-api, truncated to 16 hex characters, over IP plus User-Agent with no date. The edge’s own daily 32-bit hash is sent but ignored.
Q: Which clicks never reach purr-api at all? A: Requests the edge classes as bots, and every visit to a proxy-mode link.
Examples
The 2026-10-18 production demo walk created smo1.io/melty, clicked it once, and saw “one real boop counted; Analytics charts populated” (gate G-5-4). That boop moved links.total_clicks first and then landed in the click store; a fix the same day (purr-api #64) made both stores return 30 days for the “Boopz Over Time” chart instead of 14.
neighbors on the map
- Bot Detection & Visitor Deduplication investigating inflated click counts
- Caching Strategy: What Is Actually Cached deciding whether a stale redirect is a cache problem
- Database Architecture writing a migration or a production SQL check
- Gamification: Pawprintz, Treatz & Niblz explaining link health scores to a user