Skip to content

feat(analytics): complete the PostHog migration — retire Umami, fix the MCP event contract - #10177

Merged
JSONbored merged 1 commit into
mainfrom
claude/posthog-migration-8293
Jul 31, 2026
Merged

feat(analytics): complete the PostHog migration — retire Umami, fix the MCP event contract#10177
JSONbored merged 1 commit into
mainfrom
claude/posthog-migration-8293

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

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-js replaces the hand-rolled Umami beacon, behind the same first-party /stats proxy the Umami collect endpoint used. lib/analytics.ts is the single seam — nothing else touches posthog-js.

Umami parity, audited against the removed beacon's actual payload rather than assumed:

Beacon sent PostHog
hostname / url / referrer / screen / language automatic on every event
title added explicitly — posthog-js has no $title equivalent
geo unchanged: still derived from the proxy's x-forwarded-for
navigator.doNotTrack check respect_dnt: true

Two decisions made with the sibling implementation's results already in hand:

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.

Metric discontinuity, on purpose. The beacon fired once per page load and never on SPA navigation. Pageview counts run legitimately higher after cutover.

2. MCP telemetry (#10175)

$mcp_tool_call was 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_error literally, so every event ingested cleanly and every breakdown rendered empty — with nothing surfacing the mismatch.

  • $mcp_tool_call now carries the canonical $mcp_* properties, with LoopOver's surface / transport / category alongside them. PostHog's custom-server docs sanction extra properties explicitly, and those three have no canonical equivalent.
  • usage_event is 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.
  • Adds $mcp_initialize (from the handshake's own clientInfo, not the x-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).
  • Both carry $session_id from Mcp-Session-Id, which is what makes a funnel across handshake → discovery → 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 usage_event 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 loopover-api Worker.

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 .prettierignore re-admitting only the three governed workspaces, restates the generated-file and content/docs exemptions 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.init is not widened here. npm audit reports 13 pre-existing vulnerabilities, all in the release-please / octokit chain — posthog-js appears in none of them, and a release-automation bump doesn't belong in this PR.

Verification

  • npm run typecheck (root + packages + UI) clean
  • Backend: 116 tests across the MCP telemetry, contract, and proxy suites
  • UI: 735 + 407 tests
  • ui:lint, ui:typecheck clean
  • Drift checks: mcp:tool-reference:check, ui:openapi:check, selfhost:env-reference:check, contract:api-schemas:check all pass
  • 3,634 historical gittensory pageviews (2026-06-11 → 2026-07-31) imported to the loopover project out of band, via posthog-python with historical_migration=True

Two ui-kit test files are formatted here because they were already failing their own format:check on main, which would otherwise have turned this PR red.

…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
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@loopover-orb

loopover-orb Bot commented Jul 31, 2026

Copy link
Copy Markdown
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

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bundle Report

Changes will decrease total bundle size by 1.06kB (-0.01%) ⬇️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.95MB -1.06kB (-0.01%) ⬇️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-BI_PFNXQ.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-BRNwXT9z.js (New) 956.85kB 956.85kB 100.0% 🚀
openapi.json -116 bytes 762.56kB -0.02%
assets/docs.fumadocs-spike-api-reference-CCIMMOBH.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-B7X_oshJ.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-BVVRzIUL.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-WJhVTBHZ.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/maintainer-panel-jf2W4ghb.js (New) 78.97kB 78.97kB 100.0% 🚀
assets/routes-G6Dsq_vT.js (New) 35.88kB 35.88kB 100.0% 🚀
assets/owner-panel-ChU-ccJo.js (New) 28.39kB 28.39kB 100.0% 🚀
assets/app-D9Y0FL2D.js (New) 25.82kB 25.82kB 100.0% 🚀
assets/ui-vendor-B9oip0d9.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/fairness-DVMlWA57.js (New) 23.44kB 23.44kB 100.0% 🚀
assets/miner-panel-BvMecFqe.js (New) 20.49kB 20.49kB 100.0% 🚀
assets/api._op-Cf63TBl1.js (New) 17.51kB 17.51kB 100.0% 🚀
assets/self-hosting-docs-audit-BgY-FCAm.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs-client-loader-Cdr-c1fk.js (New) 15.68kB 15.68kB 100.0% 🚀
assets/playground-panel-Bcqkmaj2.js (New) 14.35kB 14.35kB 100.0% 🚀
assets/app.audit-1kRWjReM.js (New) 10.22kB 10.22kB 100.0% 🚀
assets/app.config-generator-BPcnIBHk.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-CBVYzs0Y.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-DfSxjHhJ.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-DypXJ7Oc.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/proof._owner._repo-DIHQV4PO.js (New) 7.11kB 7.11kB 100.0% 🚀
assets/commands-panel-zTJHeqOh.js (New) 6.74kB 6.74kB 100.0% 🚀
assets/maintainer-workflow-QsTif9ok.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/docs-nav-CjojiXeQ.js (New) 6.19kB 6.19kB 100.0% 🚀
assets/digest-panel-CZg6A_XJ.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-DlnM2ipd.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs.index-CwsJ3txE.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/api.index-CbhwvH0c.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-Ce_OKQ88.js (New) 3.01kB 3.01kB 100.0% 🚀
assets/api-BYHqTYZ4.js (New) 2.66kB 2.66kB 100.0% 🚀
assets/docs-page-OSWNf6JC.js (New) 2.14kB 2.14kB 100.0% 🚀
assets/table-CYEELhRZ.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-DgU785mo.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-BI3Z5M0k.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-D_1Q4YrN.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-CQrFN_S5.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-ylR0VYSD.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/docs._slug-CLEV3Djv.js (New) 554 bytes 554 bytes 100.0% 🚀
assets/app.maintainer-Mnk5zZZo.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-B-ozWBes.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-Dhn0L0Ru.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-DDbk64wg.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-OSzXzxro.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-8QEtA5de.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-BriiYgr9.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-D-WNKIHD.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-5uyQlF6q.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-B1l1sg4p.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/git-pull-request-arrow-Ds6WALPa.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-D5U2YNjW.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-DX7nub1B.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-BRnfJSGn.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-2LCF98tI.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-zo6RAPEU.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/play-HnP43Cp0.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-CmjdgeKI.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/add-scalar-classes-DBl3BBVh.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-C6ozxZUm.js (Deleted) -957.76kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-C2Mxc7pj.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-BHT9ebLn.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-DMkmkWQZ.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-fQtQbQDk.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-DDgOuQFt.js (Deleted) -78.97kB 0 bytes -100.0% 🗑️
assets/routes-B-a-BCfD.js (Deleted) -35.88kB 0 bytes -100.0% 🗑️
assets/owner-panel-DKFKdS4k.js (Deleted) -28.42kB 0 bytes -100.0% 🗑️
assets/app-BOUmxyuX.js (Deleted) -25.82kB 0 bytes -100.0% 🗑️
assets/ui-vendor-Nh91lqJD.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/fairness-DzA_wRSY.js (Deleted) -23.44kB 0 bytes -100.0% 🗑️
assets/miner-panel-B8Cq_bTO.js (Deleted) -20.49kB 0 bytes -100.0% 🗑️
assets/api._op-DMrOJfni.js (Deleted) -17.51kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-BMYXcfiu.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs-client-loader-DFSfUeFP.js (Deleted) -15.68kB 0 bytes -100.0% 🗑️
assets/playground-panel-D71lJQby.js (Deleted) -14.35kB 0 bytes -100.0% 🗑️
assets/app.audit-CKpNUXpF.js (Deleted) -10.22kB 0 bytes -100.0% 🗑️
assets/app.config-generator-wb8re0qH.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-BAuNn_xI.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-DeFcVdw-.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-Cc4fJLJm.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/proof._owner._repo-0dejC5wF.js (Deleted) -7.11kB 0 bytes -100.0% 🗑️
assets/commands-panel-DyNXfUjg.js (Deleted) -6.74kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-BtzLiZ4J.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/docs-nav-DD0vcRbo.js (Deleted) -6.19kB 0 bytes -100.0% 🗑️
assets/digest-panel-K9ExIXCS.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-jSVZLPrr.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs.index-leJtR8kV.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/api.index-BggTJX_n.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-BQuU0RvH.js (Deleted) -3.01kB 0 bytes -100.0% 🗑️
assets/api-DCdIP3WA.js (Deleted) -2.66kB 0 bytes -100.0% 🗑️
assets/docs-page-B1Lqob_o.js (Deleted) -2.14kB 0 bytes -100.0% 🗑️
assets/table-Cyf0sGb-.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-C3Svwpzh.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-BexoIimw.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-BL---GIE.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-CdRnuzgH.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-BH_vNzDD.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/docs._slug-BpKOHiLj.js (Deleted) -554 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-OsgTGgRA.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-c1IzxpVl.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-CVOnBqty.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-DSGDmZ6S.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-DBgvptCG.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-gHJwFgAY.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-CzEBSQh5.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-B3ahJ-6Q.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-Dxjq7Zko.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-BQCKxRq6.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-bNxPPdfR.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-Bf03r3fQ.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-C9jGZPxV.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-D_xSdEi8.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-C4OlUxye.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-Ds4kFkaq.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/play-CPMo0rVO.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-C6YxGaLE.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️

@JSONbored
JSONbored merged commit 2cafde3 into main Jul 31, 2026
8 of 9 checks passed
@JSONbored
JSONbored deleted the claude/posthog-migration-8293 branch July 31, 2026 11:20
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
26310 1 26309 21
View the top 1 failed test(s) by shortest run time
test/integration/api.test.ts > api routes > serves private MCP tool listing and tool calls
Stack Traces | 0.487s run time
AssertionError: expected 500 to be 200 // Object.is equality

- Expected
+ Received

- 200
+ 500

 ❯ test/integration/api.test.ts:5098:44

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment