Skip to content

Group session-less MCP calls and record Codex model in analytics - #240

Merged
masnwilliams merged 2 commits into
mainfrom
hypeship/derive-mcp-analytics-sessions
Oct 9, 2026
Merged

masnwilliams merged 2 commits into
mainfrom
hypeship/derive-mcp-analytics-sessions

Conversation

@masnwilliams

@masnwilliams masnwilliams commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Since the move to MCP SDK v2, clients that negotiate the 2026-07-28 protocol revision (no initialize, no mcp-session-id, a fresh server per request) get a new PostHog $session_id on every tool call. Calls per session dropped from ~5.3 to ~1.4 the week of the upgrade, so session-based MCP analytics (session list, tool neighbors, calls per session) stopped working for most traffic.

The SDK's fix for this is enableConversationId, which injects a conversation_id argument into every tool and asks agents not to make parallel calls until they have one. This PR keeps that off and derives the session server-side instead:

  • When a request carries no MCP session, eventProperties computes a key from the authenticated caller (Clerk user ID, or the API key), the organization, and the client name from request metadata. beforeSend hashes that key with a fixed 30-minute window into $session_id and the matching anonymous distinct_id. Custom events (feedback, capability reports) use the same key.
  • Requests that carry a transport session are unchanged.
  • The key is hashed before it leaves eventProperties and is removed before capture; no credential or user ID is sent.

It also records $mcp_llm_model (source client_metadata) when Codex sends x-codex-turn-metadata.model on a tool call. captureModel stays off, so no llm_model argument is injected and agents are never asked to self-report. Values must look like a model ID ([\w.:/@-]{1,100}, not unknown).

Tradeoffs

  • Sessions are fixed 30-minute windows, not inactivity-based. A run that crosses a window boundary splits in two, and back-to-back runs in one window merge. Inactivity-based sessions would need shared state and a Redis round-trip on every session-less tool call, which isn't worth it for analytics.
  • Concurrent runs by the same caller and client in one window share a session.
  • Model coverage is Codex only. Other clients will still show as unknown.

Testing

  • bun test: 910 pass. New integration tests send 2026-07-28 requests through createMcpHandler and check that calls from the same caller and client share a session (and distinct ID), that a different caller or client gets a different one, that a carried transport session is kept, and that the model is recorded only from valid Codex metadata. The grouping test fails without the change.
  • tsc --noEmit and prettier pass.
  • Not yet verified against production traffic; check calls per session by protocol version in PostHog after deploy.

Note

Medium Risk
Changes how all session-less MCP traffic is attributed in PostHog (distinct ids and session metrics); logic is hashed and allowlisted but mis-bucketing could merge or split sessions incorrectly.

Overview
Restores meaningful session-scoped MCP analytics for the 2026-07-28 protocol (no transport mcp-session-id, fresh server per request) by deriving $session_id server-side instead of enabling SDK enableConversationId.

When a request has no real MCP session, instrumentation attaches a transient hashed __mcp_analytics_session_key from caller identity (Clerk user or API token), organization, and client name from request metadata. sanitizeMcpAnalyticsEvent buckets that key into 30-minute fixed windows to set $session_id and align anonymous distinct_id; the internal key is stripped before capture. Transport sessions are unchanged. Custom events (e.g. feedback) use the same derivation.

Also records $mcp_llm_model with source client_metadata on tools/call when Codex sends x-codex-turn-metadata.model, with strict validation (no agent self-report).

Tests add 2026-07-28 createMcpHandler flows: shared sessions per caller/client/window, feedback in the same derived session, preserved carried session, and Codex-only model properties.

Reviewed by Cursor Bugbot for commit 11effae. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
mcp Ready Ready Preview Oct 9, 2026 2:58pm UTC

@rgarcia rgarcia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reviewed — looks good. two small test things, neither blocking:

Nits

  • src/lib/mcp/analytics.test.ts:1647-1664 — first and second run on the real clock, so if the test crosses a :00/:30 window boundary they land in different sessions and the equality assertion fails. rare, but consider pinning time with setSystemTime from bun:test.
  • src/lib/mcp/analytics.ts:563 — nothing covers captureMcpCustomEvent (feedback / capability reports) getting the derived session. might be worth a test asserting a custom event lands in the same $session_id as a tool call from the same caller + client.

@masnwilliams

Copy link
Copy Markdown
Collaborator Author

Thanks, both addressed in the latest commit:

  • The grouping test now pins the clock with setSystemTime, so it can't cross a window boundary. It also moves the clock 30 minutes forward and checks that the next window gets a new session.
  • New test: a feedback report on a session-less request lands in the same $session_id and distinct_id as a tool call from the same caller and client. It fails if captureMcpCustomEvent stops passing the session key.

@masnwilliams
masnwilliams merged commit 0a30523 into main Oct 9, 2026
10 checks passed
@masnwilliams
masnwilliams deleted the hypeship/derive-mcp-analytics-sessions branch October 9, 2026 15:12

This branch was successfully deployed

1 active deployment
Preview — 11effae0 Deployed Oct 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants