feat(analytics): capture $pageleave and send $pathname - #60
Merged
Conversation
Measured over the shared PostHog project, 208 of 432 sessions across the five instrumented sites record zero duration, and PostHog's built-in Web analytics Page/Entry page/Exit page tables return zero rows. capture_pageleave was false, so a session's last recorded timestamp is its last pageview, and a five-minute read of one page scores as zero seconds. Flipping the option alone fixes nothing: sanitizeEvent allowlisted only $pageview, cta activated, and $web_vitals, so every $pageleave posthog-js emitted would have been dropped silently with no error and no ingestion warning. This adds a $pageleave branch that rebuilds the event the same way $pageview does. capture_pageview is false here (pageviews are captured by hand), so posthog-js's _shouldCapturePageleave gate needs an explicit true rather than the default. $pathname is the property PostHog's page tables actually key off, and it was never sent. It's bound to the already-sanitized `path` value, never the raw pathname, so it can't carry a route outside ALLOWED_ROUTES and adds no information the event wasn't already sending. A regression test asserts the two never diverge. No privacy option changes: cookieless_mode, person_profiles, persistence, disable_persistence, respect_dnt, save_referrer, and save_campaign_params are untouched. Part of X16 in the ops execution plan.
Collaborator
Author
|
@coderabbitai review |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
biggest-littlest
approved these changes
Aug 26, 2026
ALARGECOMPANY
approved these changes
Aug 26, 2026
Collaborator
Author
|
@coderabbitai review |
scttbnsn
added a commit
that referenced
this pull request
Aug 28, 2026
…o production (#61) * fix(analytics): forward $raw_user_agent and $host for cookieless ingestion (#52) PostHog's cookieless server-hash step reads $raw_user_agent and $host straight off event.properties and drops the event with a cookieless_missing_user_agent/cookieless_missing_host ingestion warning if either is absent. createCommonProperties rebuilt an allowlisted properties object that dropped both, so every event was silently discarded at ingestion. Forward them through; never add $ip, which PostHog's capture service fills in server-side from the connection. Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com> * chore(config): remove the stale Cursor rules (#54) * fix(analytics): promote cookieless ingestion fix to production (#53) PostHog's cookieless server-hash step reads $raw_user_agent and $host straight off event.properties and drops the event with a cookieless_missing_user_agent/cookieless_missing_host ingestion warning if either is absent. createCommonProperties rebuilt an allowlisted properties object that dropped both, so every event was silently discarded at ingestion. Forward them through; never add $ip, which PostHog's capture service fills in server-side from the connection. Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com> * chore(config): drop the stale Cursor rules folder * docs(config): drop dangling .cursorrules references --------- Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com> * docs(readme): describe what this repo is and how it deploys (#55) * chore(gitignore): ignore the root .vercel link and history-backup bundles * feat(analytics): capture $pageleave and send $pathname (#60) Measured over the shared PostHog project, 208 of 432 sessions across the five instrumented sites record zero duration, and PostHog's built-in Web analytics Page/Entry page/Exit page tables return zero rows. capture_pageleave was false, so a session's last recorded timestamp is its last pageview, and a five-minute read of one page scores as zero seconds. Flipping the option alone fixes nothing: sanitizeEvent allowlisted only $pageview, cta activated, and $web_vitals, so every $pageleave posthog-js emitted would have been dropped silently with no error and no ingestion warning. This adds a $pageleave branch that rebuilds the event the same way $pageview does. capture_pageview is false here (pageviews are captured by hand), so posthog-js's _shouldCapturePageleave gate needs an explicit true rather than the default. $pathname is the property PostHog's page tables actually key off, and it was never sent. It's bound to the already-sanitized `path` value, never the raw pathname, so it can't carry a route outside ALLOWED_ROUTES and adds no information the event wasn't already sending. A regression test asserts the two never diverge. No privacy option changes: cookieless_mode, person_profiles, persistence, disable_persistence, respect_dnt, save_referrer, and save_campaign_params are untouched. Part of X16 in the ops execution plan. * fix(seo): repair JSON-LD logo 404 and double-slash base URLs - fix(seo): point Organization.logo at /icon-512x512.png; the referenced /logos/codeswhat-logo-green.png never existed, so crawlers got a 404 - fix(seo): strip trailing slashes from BASE_URL and reuse it in robots.ts and sitemap.ts, so a NEXT_PUBLIC_SITE_URL set with a trailing slash can't emit //sitemap.xml-style URLs - chore(seo): disallow /studio/ in robots.txt; the capture pages already 404 in production but the exclusion shouldn't depend on that guard - chore(seo): 308 the stable *.vercel.app production aliases to codeswhat.com instead of serving duplicate content - fix(api): stop forwarding EmailOctopus error detail to subscribe clients; log it server-side and return a fixed message * build(deps): bump next to ^16.2.11 to clear all 35 Dependabot alerts One-line range bump; npm resolves next 16.3.3, which also pulls the patched transitive versions: postcss 8.5.23, nanoid 3.3.18, sharp 0.35.4. npm audit now reports zero vulnerabilities. No code changes needed: the app has no middleware, rewrites, server actions, CSP nonces, or next/image usage, so none of the fixed CVEs required app-side work. * build(deps): regenerate next-env.d.ts for next 16.3 - build(deps): pick up the root-params.d.ts reference next 16.3 adds - ci(hooks): pass --no-errors-on-unmatched to the biome pre-commit job so committing only biome-ignored files (like next-env.d.ts) doesn't fail * docs(roadmap): track web-analytics table coverage follow-ups (ops X37) * chore: ignore .claude/ with a tracked line (#59) It was covered only by .git/info/exclude, which protects one clone and nobody else's. Without a tracked line, `git add -A` in the parent stages a nested worktree as an embedded gitlink and `git clean -ffd` deletes it. * docs(roadmap): point acquisition-data item at the ops analytics standard * docs(roadmap): pageleave ratio is structural; note the bot-detection canary caveat --------- Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com>
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.
Two properties the analytics contract never sent, both costing real data and neither changing the privacy posture.
What's wrong today
Measured against the shared PostHog project over the first eleven days of live traffic across all five instrumented sites:
Why
capture_pageleavewasfalse, so a session's last recorded timestamp is its last pageview. A five minute read of one page scores as zero seconds.Flipping the option alone fixes nothing, which is the part worth reviewing carefully.
sanitizeEventinfrontend/lib/posthog-privacy.tsonly accepted$pageview,cta activated, and$web_vitals; anything else fell through to the closingreturn null. Every$pageleaveposthog-js emitted would have been dropped silently, no error, no ingestion warning. This PR adds a$pageleavebranch that rebuilds the event the same way$pageviewdoes.posthog-js gates the event on
capture_pageleave === true || ('if_capture_pageview' && capture_pageview)(posthog-core.ts,_shouldCapturePageleave).capture_pageviewisfalsehere because pageviews are captured by hand ininstrumentation-client.ts, so the option has to be an explicittruerather than the default.$pathnamewas never sent, and it's the property those page tables actually read.Privacy
Nothing opens up.
cookieless_mode,person_profiles,persistence,disable_persistence,respect_dnt,save_referrer, andsave_campaign_paramsare all untouched.$pathnameis set to the already-sanitizedpathvalue, never the raw pathname, so it can't carry a route outsideALLOWED_ROUTESand adds no information the event wasn't already sending. A regression test asserts the two never diverge and that an unlisted route still collapses to/_other.Tests
npm run test:posthogpasses, 17 tests. New cases:$pageleaverebuilds correctly throughsanitizeEventfrom PostHog's own automatic properties,$pageleaverequires and forwards the cookieless server-hash fields the same way$pageviewdoes, and$pathnamenever diverges frompathacross both event types.posthog-source.test.mjsnow also assertscapture_pageleave: trueverbatim ininstrumentation-client.ts.npm run check(biome + tsc) andnpm run test:scenealso pass.No CHANGELOG entry: this repo has no tagged-release ceremony.
Part of X16 in the ops execution plan. The same change already shipped for portwing (CodesWhat/portwing#214); drydock, careerrat, and sockguard are still to come.