Skip to content

feat(analytics): per-visitor distinctId + $raw_user_agent on docs page views#517

Merged
Ehesp merged 3 commits into
mainfrom
feat/posthog-client-classification
Jul 7, 2026
Merged

feat(analytics): per-visitor distinctId + $raw_user_agent on docs page views#517
Ehesp merged 3 commits into
mainfrom
feat/posthog-client-classification

Conversation

@claude

@claude claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Requested by Ben Cook, Elliot Hesp · Slack thread

Fixes #

Summary

Before: every docs:page_view sent to PostHog was tagged with the repo as the "user" (distinctId = owner/repo), so PostHog couldn't count unique visitors and its numbers couldn't be reconciled against Plausible. Server-side captures also never set $raw_user_agent, so PostHog's own bot/traffic-type detection never engaged on them.

After: the docs:page_view capture carries a per-visitor identity and the raw User-Agent, and we lean on PostHog's built-in traffic classification instead of maintaining our own bot list.

Two changes on docs:page_view (app/src/pages/[[...path]].tsx):

  • distinctId is now a per-visitor hash — a daily-rotating SHA-256 of IP + User-Agent (YYYY-MM-DD:ip:ua, UTC), mirroring Plausible's visitor definition, instead of the owner/repo string. This lets PostHog count unique visitors and reconcile with Plausible. It stays cookieless with $process_person_profile: false for GDPR; daily rotation is deliberate, so cross-day retention is intentionally not derivable (the same privacy tradeoff Plausible makes).
  • $raw_user_agent is now sent — PostHog reads coalesce($raw_user_agent, $user_agent), so passing the raw UA lets PostHog's OWN bot/traffic classification ($virt_traffic_type, isLikelyBot, and web analytics) run on these server-side posthog-node events. We rely on that rather than a custom classifier — there is no client_type property.

owner / repository / ref / path / has_agent and $process_person_profile: false are retained.

This PR is rebased on main and incorporates #509, which removed the docs:bundle_view capture — that event is intentionally not re-added here.

How

  • app/src/lib/posthog.tsvisitorId() (SHA-256 of YYYY-MM-DD:ip:ua, UTC) and readVisitorHeaders() (prefers x-forwarded-for, falls back to x-real-ip).
  • Wired into docs:page_view (app/src/pages/[[...path]].tsx): distinctId uses visitorId() and the capture adds $raw_user_agent.
  • Unit tests for both helpers. Lint (biome) clean, typecheck adds no new errors, next build green.

Scope

  • app/ (hosted site, MCP, Ask AI)
  • packages/cli/
  • packages/mdx-bundler/
  • docs/ (product documentation)
  • Repo / CI / other

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Refactor / chore

Test plan

  • bun run check passes locally
  • Tested locally (bun dev, CLI command, or other relevant command)
  • Updated docs/ (if user-facing)
  • Verified on a docs.page URL or local preview (if rendering/routing changed)

Unit tests added for visitorId() and readVisitorHeaders(). Lint (biome) is clean, typecheck adds no new errors, and next build is green.

Notes for reviewers

Analytics remains cookieless ($process_person_profile: false) and the daily-rotating visitor hash is a deliberate privacy tradeoff (matching Plausible) — cross-day retention is intentionally not derivable. Bot/traffic-type filtering is delegated to PostHog's own $virt_traffic_type detection rather than a hand-maintained crawler list.

Follow-ups (not in this PR):

  • Rename has_agentrepo_agent_enabled (it's a per-repo AI-feature flag, not traffic classification).
  • Move capture into the edge middleware with a surface/route_type property, so the machine routes (llms.txt, raw .md, search.json, sitemap) PostHog is currently blind to are covered and captured consistently with Plausible.
  • Group analytics: model organization (owner) and publisher (owner/repo) as PostHog groups.
  • Reconciliation check: run PostHog "unique visitors, html, prod, bots excluded" against Plausible visitors per repo until they track, then retire Plausible.

Generated by Claude Code

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@railway-app railway-app Bot temporarily deployed to docs.page / docs.page-pr-517 July 3, 2026 14:24 Destroyed
@alexdukeinvertase alexdukeinvertase requested a review from Ehesp July 3, 2026 14:26
@railway-app railway-app Bot temporarily deployed to docs.page / docs.page-pr-517 July 3, 2026 14:36 Destroyed
@railway-app

railway-app Bot commented Jul 3, 2026

Copy link
Copy Markdown

🚅 Deployed to the docs.page-pr-517 environment in docs.page

Service Status Web Updated (UTC)
docs.page ✅ Success (View Logs) Web Jul 3, 2026 at 4:05 pm

claude added 3 commits July 3, 2026 16:00
…docs events

Add a client-type classifier (human / ai_agent / crawler / automation) and a
daily-rotating, cookieless visitor dedup key that mirrors Plausible's visitor
definition, then wire both into the server-side docs:page_view and
docs:bundle_view captures.

- classifyClient(userAgent): ordered ai_agent -> crawler -> automation -> human
  matching, with maintainable exported pattern lists sourced from ai.robots.txt
  and Dark Visitors.
- visitorId(ip, userAgent, date): SHA-256 of `${YYYY-MM-DD}:${ip}:${ua}` (UTC),
  used as the PostHog distinctId instead of owner/repo so unique visitors can be
  reconciled with Plausible. Daily rotation is deliberate for privacy.
- readVisitorHeaders(headers): reads x-forwarded-for/x-real-ip + user-agent from
  a Web Headers object at both existing capture sites (no move to middleware).
- Keeps owner/repository/ref/path/has_agent and $process_person_profile: false.

Follow-ups (out of scope): rename has_agent, move capture into middleware,
add group analytics, instrument machine routes.
Send $raw_user_agent on server-side docs:bundle_view and docs:page_view
captures so PostHog's own bot/traffic classification (isLikelyBot,
getTrafficType, the $virt_* virtual properties, and web analytics) can
run on these posthog-node events. PostHog reads
coalesce($raw_user_agent, $user_agent); without $raw_user_agent none of
its native filtering engaged, so our numbers could not reconcile with
PostHog web analytics.

Also close two false negatives in classifyClient where browser-like UAs
leaked into "human": HeadlessChrome/PhantomJS/Puppeteer/Playwright/
Selenium/Cypress now classify as automation, and Chrome-Lighthouse as a
crawler, matching PostHog's headless_browser and seo_crawler definitions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jf4C7K79nAbcswET4CAVJa
Per maintainer decision, remove the custom classifyClient classifier and
its client_type property. PostHog's own bot/traffic-type detection
($virt_traffic_type), fed by $raw_user_agent, is used instead. The
docs:page_view capture keeps the per-visitor visitorId distinctId and
$raw_user_agent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jf4C7K79nAbcswET4CAVJa
@claude claude Bot force-pushed the feat/posthog-client-classification branch from f6edbdd to 4259459 Compare July 3, 2026 16:04
@railway-app railway-app Bot temporarily deployed to docs.page / docs.page-pr-517 July 3, 2026 16:04 Destroyed
@claude claude Bot changed the title feat(analytics): classify client type and dedupe visitors on PostHog docs events feat(analytics): per-visitor distinctId + $raw_user_agent on docs page views Jul 3, 2026
@claude claude Bot marked this pull request as ready for review July 3, 2026 16:18
@Ehesp Ehesp merged commit b17bc2e into main Jul 7, 2026
3 checks passed
@Ehesp Ehesp deleted the feat/posthog-client-classification branch July 7, 2026 10:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants