feat(analytics): complete the PostHog migration — retire Umami, fix the MCP event contract - #10177
Merged
Merged
Conversation
…he MCP event contract Umami is fully decommissioned and PostHog is the only analytics sink. Web analytics (#8293, #8299). posthog-js replaces the hand-rolled Umami beacon in routes/__root.tsx, behind the same first-party /stats proxy the Umami collect endpoint used. lib/analytics.ts is the single seam; the SDK is loaded via a dynamic import so an unconfigured build pays zero bytes, matching browser-sentry.ts. Env is read at call time, not module scope, so the token gate is stubbable. Umami parity was audited against the removed beacon's actual payload: posthog-js supplies url/referrer/screen/language automatically, page_title is added explicitly (it has no $title equivalent), geo still comes from the proxy's x-forwarded-for, and respect_dnt replaces the beacon's own doNotTrack check. persistence is "localStorage", not "memory" — memory resets identity every reload and inflates unique visitors, the regression JSONbored/metagraphed#8210 had to correct. cookieless_mode was rejected on its documented behavior: it strips the IP before GeoIP runs, which would drop the country data #8299 requires. The proxy keeps its allowlist, one level looser (path prefixes, not exact paths) so a posthog-js upgrade cannot silently break capture. Its original justification was Umami-specific — that host served its admin API from the collect origin — which does not carry over, but bounding a public unauthenticated route still does. Session-replay snapshots get their own 2 MiB ceiling so enabling #8295 cannot silently 413. One behavior deliberately changes: the beacon fired once per page load and never on SPA navigation, so pageview counts run legitimately higher after cutover. MCP telemetry (#10175). $mcp_tool_call was emitting LoopOver's own snake_case property names under PostHog's reserved event name, so their built-in MCP dashboards ingested every event and rendered every breakdown empty. It now carries the canonical $mcp_* properties, with LoopOver's surface/transport/ category alongside them (PostHog's custom-server docs sanction extra properties, and those three have no canonical equivalent). usage_event is untouched — it stays the minimal, payload-free LoopOver event in its own vocabulary, and the allowlist meta-test now checks the two vocabularies separately. Adds the two missing canonical events: $mcp_initialize, sourced from the handshake's own clientInfo rather than the x-loopover-mcp-* headers that only our published client sets, and $mcp_tools_list, reporting what this server registered. Both carry $session_id from Mcp-Session-Id, which is what makes a funnel across handshake, discovery, and tool calls possible at all. The closed LoopOver error-code set is projected onto PostHog's closed $mcp_error_type set so their error breakdown populates without losing the precise code. Reshaping the wire format is free right now and stops being free immediately: the project holds zero $mcp_tool_call events, because POSTHOG_API_KEY was never set on the Worker. Prettier (#10176). A root prettier run found no config, fell back to the 80-column default, and rewrapped backend source that nothing in CI formats — repeatedly, inside unrelated PRs. Adds a default-deny root .prettierignore that re-admits only the three governed workspaces, restates the generated-file and content/docs exemptions that a root run cannot inherit, and pins ui-kit to its current effective width so it is stated rather than inherited from a vendor default. Verified by running prettier --write . from the root and confirming a no-op. No existing code is reformatted. Also formats two ui-kit test files that were already failing their own format:check on main, which would otherwise have turned this PR red. Closes #8293 Closes #8299 Closes #10175 Closes #10176
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
loopover-ui | 8e4a7fb | Commit Preview URL Branch Preview URL |
Jul 31 2026, 11:06 AM |
Contributor
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Contributor
|
Important 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏳ LoopOver is waiting…LoopOver has seen this pull request and is waiting on CI checks to finish before reviewing it. This comment will update once the review runs. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed · 🟨 Waiting |
Bundle ReportChanges will decrease total bundle size by 1.06kB (-0.01%) ⬇️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: loopover-uiAssets Changed:
|
This was referenced Jul 31, 2026
❌ 1 Tests Failed:
View the top 1 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
This was referenced Jul 31, 2026
JSONbored
added a commit
that referenced
this pull request
Jul 31, 2026
dead-exports:check, part of test:ci, is red on a clean main, so every open PR inherits the failure and none can go green. Third instance of this class after #9944 and #10109. #10177 added `export const MCP_SERVER_NAME` in src/mcp/server.ts. Both readers live in that same file -- the handshake's serverInfo.name and the analytics property builder -- so the export has no consumer. CHAT_GROUNDING_MCP_SERVER_NAME in the engine is a separate symbol, not this one re-exported. No behaviour change: the constant and both call sites are untouched, and its "one constant so the handshake and the dashboards cannot disagree" rationale still holds. It is simply module-local now. Closes #10194
This was referenced Jul 31, 2026
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.
Completes the Umami → PostHog migration. Umami is fully decommissioned; PostHog is the only analytics sink.
Closes #8293
Closes #8299
Closes #10175
Closes #10176
1. Web analytics (#8293, #8299)
posthog-jsreplaces the hand-rolled Umami beacon, behind the same first-party/statsproxy the Umami collect endpoint used.lib/analytics.tsis the single seam — nothing else touchesposthog-js.Umami parity, audited against the removed beacon's actual payload rather than assumed:
hostname/url/referrer/screen/languagetitle$titleequivalentx-forwarded-fornavigator.doNotTrackcheckrespect_dnt: trueTwo decisions made with the sibling implementation's results already in hand:
persistence: "localStorage", not"memory". Memory persists nothing, so every reload mints a new anonymous identity and inflates unique visitors — the regression fix(ui): decommission Umami, fix PostHog visitor-inflation + geo/title parity metagraphed#8210 had to correct. localStorage sets no cookie, so the cookieless posture holds.cookieless_moderejected on its documented behavior, not a guess: it strips the request IP before any GeoIP enrichment runs, unconditionally, so the country data Umami decommission: gated removal of the /stats Umami path + beacon #8299 requires would never populate again.The proxy keeps its allowlist, one level looser (path prefixes, not exact paths) so a posthog-js upgrade can't silently break capture. Its original justification was Umami-specific — that host served its admin API from the collect origin — which doesn't carry over, but bounding a public unauthenticated route still does. Replay snapshots get their own 2 MiB ceiling so enabling #8295 can't silently 413.
2. MCP telemetry (#10175)
$mcp_tool_callwas emitting LoopOver's snake_case property names under PostHog's reserved event name. Their built-in MCP dashboards read$mcp_tool_name/$mcp_duration_ms/$mcp_is_errorliterally, so every event ingested cleanly and every breakdown rendered empty — with nothing surfacing the mismatch.$mcp_tool_callnow carries the canonical$mcp_*properties, with LoopOver'ssurface/transport/categoryalongside them. PostHog's custom-server docs sanction extra properties explicitly, and those three have no canonical equivalent.usage_eventis untouched — still the minimal, payload-free LoopOver event in its own vocabulary. The allowlist meta-test now checks the two vocabularies separately, so neither can quietly satisfy the other's check.$mcp_initialize(from the handshake's ownclientInfo, not thex-loopover-mcp-*headers that only our published client sets — those would collapse every third-party client into one bucket) and$mcp_tools_list(what this server registered).$session_idfromMcp-Session-Id, which is what makes a funnel across handshake → discovery → tool calls possible at all.$mcp_error_typeset, so their error breakdown populates withoutusage_eventlosing the precise code.Reshaping the wire format is free right now and stops being free immediately: the project holds zero
$mcp_tool_callevents, becausePOSTHOG_API_KEYwas never set on theloopover-apiWorker.3. Prettier (#10176)
A root prettier run found no config, fell back to the 80-column default, and rewrapped backend source that nothing in CI formats — repeatedly, inside unrelated PRs. Adds a default-deny root
.prettierignorere-admitting only the three governed workspaces, restates the generated-file andcontent/docsexemptions a root run can't inherit, and pins ui-kit to its current effective width so it's stated rather than inherited from a vendor default.No existing code is reformatted. Verified by running
prettier --write .from the root and confirming a no-op.Out of scope, deliberately
Session replay (#8295), UI error tracking (#8291), feature flags (#8297), and MCP dashboards (#8606) each stay their own issue;
posthog.initis not widened here.npm auditreports 13 pre-existing vulnerabilities, all in therelease-please/ octokit chain —posthog-jsappears in none of them, and a release-automation bump doesn't belong in this PR.Verification
npm run typecheck(root + packages + UI) cleanui:lint,ui:typecheckcleanmcp:tool-reference:check,ui:openapi:check,selfhost:env-reference:check,contract:api-schemas:checkall passloopoverproject out of band, via posthog-python withhistorical_migration=TrueTwo ui-kit test files are formatted here because they were already failing their own
format:checkonmain, which would otherwise have turned this PR red.