feat(analytics): per-visitor distinctId + $raw_user_agent on docs page views#517
Merged
Conversation
|
|
|
🚅 Deployed to the docs.page-pr-517 environment in docs.page
|
…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
f6edbdd to
4259459
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Ben Cook, Elliot Hesp · Slack thread
Fixes #
Summary
Before: every
docs:page_viewsent 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_viewcapture 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):distinctIdis 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 theowner/repostring. This lets PostHog count unique visitors and reconcile with Plausible. It stays cookieless with$process_person_profile: falsefor GDPR; daily rotation is deliberate, so cross-day retention is intentionally not derivable (the same privacy tradeoff Plausible makes).$raw_user_agentis now sent — PostHog readscoalesce($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-sideposthog-nodeevents. We rely on that rather than a custom classifier — there is noclient_typeproperty.owner/repository/ref/path/has_agentand$process_person_profile: falseare retained.This PR is rebased on
mainand incorporates #509, which removed thedocs:bundle_viewcapture — that event is intentionally not re-added here.How
app/src/lib/posthog.ts—visitorId()(SHA-256 ofYYYY-MM-DD:ip:ua, UTC) andreadVisitorHeaders()(prefersx-forwarded-for, falls back tox-real-ip).docs:page_view(app/src/pages/[[...path]].tsx):distinctIdusesvisitorId()and the capture adds$raw_user_agent.next buildgreen.Scope
app/(hosted site, MCP, Ask AI)packages/cli/packages/mdx-bundler/docs/(product documentation)Type of change
Test plan
bun run checkpasses locallybun dev, CLI command, or other relevant command)docs/(if user-facing)Unit tests added for
visitorId()andreadVisitorHeaders(). Lint (biome) is clean, typecheck adds no new errors, andnext buildis 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_typedetection rather than a hand-maintained crawler list.Follow-ups (not in this PR):
has_agent→repo_agent_enabled(it's a per-repo AI-feature flag, not traffic classification).surface/route_typeproperty, so the machine routes (llms.txt, raw.md,search.json, sitemap) PostHog is currently blind to are covered and captured consistently with Plausible.organization(owner) andpublisher(owner/repo) as PostHog groups.Generated by Claude Code