From e4134516253cb1cfc6f9644265789b6c34dca970 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 21:26:43 +0000 Subject: [PATCH 1/2] Count real analytics usage: drop connection/render-triggered events, gate MCP tool calls - Remove mcp:server_view (fired on every GET to the MCP endpoint; idle connections in configs inflated it). - Gate mcp:tool_call to actual JSON-RPC tools/call requests, ignoring protocol chatter (initialize, tools/list, notifications/*). Peek at a cloned request body so the handler still reads an intact stream; handle batched arrays and attach the tool name. - Remove agent:session_create (fired on every render for agent-enabled repos); keep session minting and cookie logic intact. Add is_first_message and session_id to agent:message_send instead. - Remove docs:bundle_view; docs:page_view remains the canonical view metric. docs:bundle_fail is untouched. --- app/src/app/api/[owner]/[repo]/mcp/route.ts | 57 +++++++++++++-------- app/src/app/api/agent/route.ts | 2 + app/src/app/api/bundle/route.ts | 13 ----- app/src/pages/[[...path]].tsx | 11 ---- 4 files changed, 39 insertions(+), 44 deletions(-) diff --git a/app/src/app/api/[owner]/[repo]/mcp/route.ts b/app/src/app/api/[owner]/[repo]/mcp/route.ts index d413cde9..7afa5b9f 100644 --- a/app/src/app/api/[owner]/[repo]/mcp/route.ts +++ b/app/src/app/api/[owner]/[repo]/mcp/route.ts @@ -80,16 +80,6 @@ export async function GET(req: Request, context: RouteContext) { } const descriptor = await createMcpDescriptor(context); - getPostHogClient()?.capture({ - distinctId: `${route.owner}/${route.repository}`, - event: "mcp:server_view", - properties: { - owner: route.owner, - repository: route.repository, - ref: route.ref ?? null, - $process_person_profile: false, - }, - }); return Response.json(descriptor, { status: 200 }); } catch (error) { if (error instanceof BundlerError) { @@ -114,16 +104,43 @@ export async function POST(req: Request, context: RouteContext) { return Response.json({ error: "Not found." }, { status: 404 }); } - getPostHogClient()?.capture({ - distinctId: `${route.owner}/${route.repository}`, - event: "mcp:tool_call", - properties: { - owner: route.owner, - repository: route.repository, - ref: route.ref ?? null, - $process_person_profile: false, - }, - }); + // Peek at the JSON-RPC body on a clone so the original request stream stays + // intact for handleMcpPost (which reads req.json() itself). Only actual + // `tools/call` requests count as MCP usage — protocol chatter such as + // `initialize`, `tools/list`, and `notifications/*` is ignored. + const parsedBody = await req + .clone() + .json() + .catch(() => undefined); + const rpcMessages = Array.isArray(parsedBody) ? parsedBody : [parsedBody]; + const posthog = getPostHogClient(); + + if (posthog) { + for (const message of rpcMessages) { + if ( + message && + typeof message === "object" && + message.method === "tools/call" + ) { + const toolName = + typeof message.params?.name === "string" + ? message.params.name + : null; + posthog.capture({ + distinctId: `${route.owner}/${route.repository}`, + event: "mcp:tool_call", + properties: { + owner: route.owner, + repository: route.repository, + ref: route.ref ?? null, + tool: toolName, + $process_person_profile: false, + }, + }); + } + } + } + return handleMcpPost(req, context); } catch (error) { if (error instanceof BundlerError) { diff --git a/app/src/app/api/agent/route.ts b/app/src/app/api/agent/route.ts index 62bdb99c..e33c215c 100644 --- a/app/src/app/api/agent/route.ts +++ b/app/src/app/api/agent/route.ts @@ -326,6 +326,8 @@ export async function POST(req: Request) { repository: session.repo, provider, message_count: messages.length, + is_first_message: messages.length === 1, + session_id: session.sid, $process_person_profile: false, }, }); diff --git a/app/src/app/api/bundle/route.ts b/app/src/app/api/bundle/route.ts index 77a6f4ff..f9b36c88 100644 --- a/app/src/app/api/bundle/route.ts +++ b/app/src/app/api/bundle/route.ts @@ -45,19 +45,6 @@ export async function GET(req: Request) { }), ]); - getPostHogClient()?.capture({ - distinctId: `${input.data.owner}/${input.data.repository}`, - event: "docs:bundle_view", - properties: { - owner: input.data.owner, - repository: input.data.repository, - ref: input.data.ref ?? null, - path: input.data.path, - has_agent: hasAgent, - $process_person_profile: false, - }, - }); - const response = Response.json( { code: "OK", diff --git a/app/src/pages/[[...path]].tsx b/app/src/pages/[[...path]].tsx index 6f57dfe3..73b6c6a8 100644 --- a/app/src/pages/[[...path]].tsx +++ b/app/src/pages/[[...path]].tsx @@ -339,17 +339,6 @@ export const getServerSideProps = (async ({ params, req, res, query }) => { csrfCookiePath, ), ); - - getPostHogClient()?.capture({ - distinctId: `${route.owner}/${route.repository}`, - event: "agent:session_create", - properties: { - owner: route.owner, - repository: route.repository, - ref: route.ref ?? null, - $process_person_profile: false, - }, - }); } getPostHogClient()?.capture({ From 14fe149a4e78fbdd9ff7421855e8d6eb09205995 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 09:19:11 +0000 Subject: [PATCH 2/2] refactor: capture mcp:tool_call inside tool handlers Move the mcp:tool_call analytics capture out of the POST route's JSON-RPC body peek and into the per-request MCP tool handlers. This removes the double body read (req.clone().json()) and ensures the event only fires when a tool actually executes. --- app/src/app/api/[owner]/[repo]/mcp/route.ts | 38 --------------------- app/src/server/mcp/server.ts | 17 +++++++++ 2 files changed, 17 insertions(+), 38 deletions(-) diff --git a/app/src/app/api/[owner]/[repo]/mcp/route.ts b/app/src/app/api/[owner]/[repo]/mcp/route.ts index 7afa5b9f..6cabe37b 100644 --- a/app/src/app/api/[owner]/[repo]/mcp/route.ts +++ b/app/src/app/api/[owner]/[repo]/mcp/route.ts @@ -1,5 +1,4 @@ import { resolveDocsRoute } from "@/lib/docs-routing"; -import { getPostHogClient } from "@/lib/posthog"; import { BundlerError } from "@/server/docs/bundle"; import { loadDocsConfigForResolvedSha } from "@/server/docs/source-dataset"; import { listGitHubDocFiles } from "@/server/github/tree"; @@ -104,43 +103,6 @@ export async function POST(req: Request, context: RouteContext) { return Response.json({ error: "Not found." }, { status: 404 }); } - // Peek at the JSON-RPC body on a clone so the original request stream stays - // intact for handleMcpPost (which reads req.json() itself). Only actual - // `tools/call` requests count as MCP usage — protocol chatter such as - // `initialize`, `tools/list`, and `notifications/*` is ignored. - const parsedBody = await req - .clone() - .json() - .catch(() => undefined); - const rpcMessages = Array.isArray(parsedBody) ? parsedBody : [parsedBody]; - const posthog = getPostHogClient(); - - if (posthog) { - for (const message of rpcMessages) { - if ( - message && - typeof message === "object" && - message.method === "tools/call" - ) { - const toolName = - typeof message.params?.name === "string" - ? message.params.name - : null; - posthog.capture({ - distinctId: `${route.owner}/${route.repository}`, - event: "mcp:tool_call", - properties: { - owner: route.owner, - repository: route.repository, - ref: route.ref ?? null, - tool: toolName, - $process_person_profile: false, - }, - }); - } - } - } - return handleMcpPost(req, context); } catch (error) { if (error instanceof BundlerError) { diff --git a/app/src/server/mcp/server.ts b/app/src/server/mcp/server.ts index 17b59e0f..4c68e548 100644 --- a/app/src/server/mcp/server.ts +++ b/app/src/server/mcp/server.ts @@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp"; import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp"; import * as z from "zod"; import type { ResolvedDocsRoute } from "@/lib/docs-routing"; +import { getPostHogClient } from "@/lib/posthog"; import type { Config } from "@/server/config"; import { ConfigSchema } from "@/server/config/schema"; import { BundlerError } from "@/server/docs/bundle"; @@ -118,6 +119,20 @@ export async function createMcpDescriptor(context: McpRepoContext) { async function createMcpServer(context: McpRepoContext) { const { route, docList } = context; + const posthog = getPostHogClient(); + const captureToolCall = (tool: string) => { + posthog?.capture({ + distinctId: `${route.owner}/${route.repository}`, + event: "mcp:tool_call", + properties: { + owner: route.owner, + repository: route.repository, + ref: route.ref ?? null, + tool, + $process_person_profile: false, + }, + }); + }; const [metadata, skillList] = await Promise.all([ Promise.resolve(getMcpServerMetadata(context)), getMcpSkillResources(route), @@ -213,6 +228,7 @@ async function createMcpServer(context: McpRepoContext) { }, } as unknown as never, (async (args: unknown) => { + captureToolCall("read_doc_page"); const { path } = ReadDocToolSchema.parse(args); try { @@ -260,6 +276,7 @@ async function createMcpServer(context: McpRepoContext) { inputSchema: {}, } as unknown as never, (async (args: unknown) => { + captureToolCall("list_doc_files"); ListDocFilesToolSchema.parse(args ?? {}); return {