Repository navigation
fix(events): give PostHog the visitor's user agent and country - #21
Merged
Merged
Conversation
PostHog classed every server-sent event as a bot, placed every visitor at the Cloud Run egress in Brussels, and merged all unconsented visitors into one 'anonymous' person. Each track() caller now passes the browser request's headers, so sendEventToPosthog() can send $raw_user_agent, $ip for consented visitors only, and $geoip_disable otherwise. An event with no visitor id and no user id gets a random distinct_id and $process_person_profile false. Event props can no longer set PostHog's $ properties.
GA4 already locates consented visitors, so PostHog needs only the country, joined to product events. Every event now carries `country` from the CF-IPCountry header and sets $geoip_disable, and no event sends the visitor's IP.
Search events fired once per query text from a React effect, so edits, repeats, failures and abandoned searches recorded nothing, and no event could be joined to the engine's log line. Each post (the base run and every edit) now gets a tracker in useSearchRun that sends exactly one terminal event from the post's own stream: search.query or search.empty for a base run, search.edit for an edit, search.error, or search.abandon (hidden, superseded or reset). Every event carries searchId, post, source, traceId, the client and engine times and the engine's outcome. search.action records reading picks, downloads and shares, search.start_point replaces the starting point's search.query, and shareSearch now sends share.create with kind search.
Nothing counted a share link being opened: embeds had embed.view, but the /s/<id> page sent no event. The page now records share.open with shareId, kind, mode and the referring host, server-side and with no visitor or user id, scheduled before a search share's redirect. refererHost moves to src/lib/events/referer.ts so the embed page and the share page share it.
…on ids track() now writes each event as one JSON log line (message "event", name, props, page path, signedIn, no user or visitor id) before the database insert and the PostHog forward, so events can be joined to the dfo-apis resolve line by traceId in SQL. Leaving the search page mid-search ended no post, and the departed post later sent its result: the hook now ends active posts as a "left" abandon and aborts their streams on a real unmount, deferred one microtask so Strict Mode's dev remount does not abort a permalink run. Share and download actions read the search ids when the request starts, not when it lands. The documented true-abandon query now also excludes posts that later ended as superseded, reset or left.
The browser track() read location.pathname when it sent, so a post abandoned by navigating away was recorded under the destination page. Each post and each search action now captures its page when it starts and sends that page on every event; track() takes an optional page.
crypto.randomUUID exists only in secure contexts, so search ids and the visitor id threw on the LAN dev server; both use uuid(). A share lookup or ?s= decode that resolved after the visitor left /data still started a search and sent its events. The unmount guard now exposes left(), and startRun and showStartingPoint refuse to start once it is set, which covers every link initialiser. The homepage prompt sends /data?q=&via=typed|example. /data reads via once, strips it from the address bar, and records that source, so only a shared or bookmarked ?q= link records permalink.
jm-rivera
marked this pull request as ready for review
October 20, 2026 15:26
…-tracking feat(search): send one usage event per search post, joined by trace id
Preserve search source tracking with streamed suggestions, and retain share.open and gallery.open in the event registry.
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.
Server-side events sent to PostHog carry the browser's
$raw_user_agentand Cloudflare country, so bot classification and country breakdowns reflect the visitor. GeoIP is disabled and the visitor's IP is omitted. Events with neither a visitor id nor an account id receive a fresh UUID and$process_person_profile: false. Client-supplied properties beginning with$are stripped before forwarding.All six server callers pass the incoming request headers, including the sign-in callback, share page loads, and embed renders. The embed page reads its headers during render for use in
after(). The AI policy and analytics documentation describe the country and browser data collected.Search runs and edits record their source, post number, trace id, duration, and outcome. Tracking covers failures, abandonment, and actions on the answer displayed when the action starts. Share creation and opening are recorded, and each usage event is also written as a structured JSON line for Cloud Logging.
Visitor-to-account linking and browser page/session enrichment remain outside this change.
Testing: All 1,613 tests passed locally against PostgreSQL 18.
pnpm typecheck,pnpm lint,pnpm lint:css,pnpm build:migrate, and the migration-bundle smoke test passed. GitHub CI passed, including the production build.