From 0260d085142e50beec8c8738379755223ccc870d Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 13 Apr 2026 16:58:11 -0700 Subject: [PATCH 01/14] feat(web): rename PostHog chat events, add tool_used tracking, and add client-side ask events Renames server-side chat events to drop the wa_ prefix (since they fire from multiple sources), adds a new tool_used event for unified tool call tracking across the ask agent and MCP server, and introduces client-side wa_ask_* events for accurate web-only usage dashboards. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 6 +++++ .../web/src/app/api/(server)/chat/route.ts | 7 +++-- packages/web/src/features/chat/actions.ts | 3 ++- packages/web/src/features/chat/agent.ts | 9 +------ .../chat/components/chatThread/chatThread.tsx | 5 ++++ .../features/chat/useCreateNewChatThread.ts | 4 +++ packages/web/src/features/mcp/askCodebase.ts | 6 +++-- packages/web/src/features/mcp/server.ts | 17 ++++++++++++ packages/web/src/features/tools/adapters.ts | 27 ++++++++++++++++++- packages/web/src/lib/posthogEvents.ts | 17 +++++++++--- 10 files changed, 83 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 943898eae..21f044578 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,6 +57,12 @@ if (!value) { return; } if (condition) doSomething(); ``` +## PostHog Event Naming + +- The `wa_` prefix is reserved for events that can ONLY be fired from the web app (e.g., `wa_login_with_github`, `wa_chat_feedback_submitted`). +- Events fired from multiple sources (web app, MCP server, API) must NOT use the `wa_` prefix (e.g., `ask_message_sent`, `tool_used`). +- Multi-source events should include a `source` property to identify the origin (e.g., `'sourcebot-web-client'`, `'sourcebot-mcp-server'`, `'sourcebot-ask-agent'`). + ## Tailwind CSS Use Tailwind color classes directly instead of CSS variable syntax: diff --git a/packages/web/src/app/api/(server)/chat/route.ts b/packages/web/src/app/api/(server)/chat/route.ts index 2ae2f9700..4c0b12819 100644 --- a/packages/web/src/app/api/(server)/chat/route.ts +++ b/packages/web/src/app/api/(server)/chat/route.ts @@ -91,12 +91,15 @@ export const POST = apiHandler(async (req: NextRequest) => { return []; }))).flat(); - await captureEvent('wa_chat_message_sent', { + const source = req.headers.get('X-Sourcebot-Client-Source') ?? undefined; + + await captureEvent('ask_message_sent', { chatId: id, messageCount: messages.length, selectedReposCount: expandedRepos.length, + source, ...(env.EXPERIMENT_ASK_GH_ENABLED === 'true' ? { selectedRepos: expandedRepos } : {}), - } ); + }); const stream = await createMessageStream({ chatId: id, diff --git a/packages/web/src/features/chat/actions.ts b/packages/web/src/features/chat/actions.ts index 3762e88c3..dac24d155 100644 --- a/packages/web/src/features/chat/actions.ts +++ b/packages/web/src/features/chat/actions.ts @@ -49,9 +49,10 @@ export const createChat = async ({ source }: { source?: string } = {}) => sew(() }); } - await captureEvent('wa_chat_thread_created', { + await captureEvent('ask_thread_created', { chatId: chat.id, isAnonymous: isGuestUser, + source, }); return { diff --git a/packages/web/src/features/chat/agent.ts b/packages/web/src/features/chat/agent.ts index 2e2676299..784ca5344 100644 --- a/packages/web/src/features/chat/agent.ts +++ b/packages/web/src/features/chat/agent.ts @@ -1,7 +1,6 @@ import { SBChatMessage, SBChatMessageMetadata } from "@/features/chat/types"; import { getAnswerPartFromAssistantMessage } from "@/features/chat/utils"; import { getFileSource } from '@/features/git'; -import { captureEvent } from "@/lib/posthog"; import { isServiceError } from "@/lib/utils"; import { LanguageModelV3 as AISDKLanguageModelV3 } from "@ai-sdk/provider"; import { ProviderOptions } from "@ai-sdk/provider-utils"; @@ -210,13 +209,7 @@ const createAgentStream = async ({ ], toolChoice: "auto", onStepFinish: ({ toolResults }) => { - toolResults.forEach(({ toolName, output, dynamic }) => { - captureEvent('wa_chat_tool_used', { - chatId, - toolName, - success: !isServiceError(output), - }); - + toolResults.forEach(({ output, dynamic }) => { if (dynamic || isServiceError(output)) { return; } diff --git a/packages/web/src/features/chat/components/chatThread/chatThread.tsx b/packages/web/src/features/chat/components/chatThread/chatThread.tsx index 1cdf8ef0b..b7f8e1447 100644 --- a/packages/web/src/features/chat/components/chatThread/chatThread.tsx +++ b/packages/web/src/features/chat/components/chatThread/chatThread.tsx @@ -126,6 +126,11 @@ export const ChatThread = ({ .map((part) => part.data); setSources((prev) => [...prev, ...sources]); + captureEvent('wa_ask_message_sent', { + chatId, + selectedReposCount: selectedSearchScopes.length, + }); + _sendMessage(message, { body: { selectedSearchScopes, diff --git a/packages/web/src/features/chat/useCreateNewChatThread.ts b/packages/web/src/features/chat/useCreateNewChatThread.ts index 63ead0249..951f03c84 100644 --- a/packages/web/src/features/chat/useCreateNewChatThread.ts +++ b/packages/web/src/features/chat/useCreateNewChatThread.ts @@ -46,6 +46,10 @@ export const useCreateNewChatThread = ({ isAuthenticated = false }: UseCreateNew return; } + captureEvent('wa_ask_thread_created', { + chatId: response.id, + }); + setChatState({ inputMessage, selectedSearchScopes, diff --git a/packages/web/src/features/mcp/askCodebase.ts b/packages/web/src/features/mcp/askCodebase.ts index 6ca9c26a2..6451e680f 100644 --- a/packages/web/src/features/mcp/askCodebase.ts +++ b/packages/web/src/features/mcp/askCodebase.ts @@ -86,9 +86,10 @@ export const askCodebase = (params: AskCodebaseParams): Promise r.value) } : {}), diff --git a/packages/web/src/features/mcp/server.ts b/packages/web/src/features/mcp/server.ts index 1de4efee4..ec4103f36 100644 --- a/packages/web/src/features/mcp/server.ts +++ b/packages/web/src/features/mcp/server.ts @@ -2,6 +2,7 @@ import { languageModelInfoSchema, } from '@/features/chat/types'; import { askCodebase } from '@/features/mcp/askCodebase'; +import { captureEvent } from '@/lib/posthog'; import { isServiceError } from '@/lib/utils'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { ChatVisibility } from '@sourcebot/db'; @@ -57,6 +58,11 @@ export async function createMcpServer(): Promise { }, async () => { const models = await getConfiguredLanguageModelsInfo(); + captureEvent('tool_used', { + toolName: 'list_language_models', + source: 'sourcebot-mcp-server', + success: true, + }); return { content: [{ type: "text", text: JSON.stringify(models) }] }; } ); @@ -101,11 +107,22 @@ export async function createMcpServer(): Promise { }); if (isServiceError(result)) { + captureEvent('tool_used', { + toolName: 'ask_codebase', + source: 'sourcebot-mcp-server', + success: false, + }); return { content: [{ type: "text", text: `Failed to ask codebase: ${result.message}` }], }; } + captureEvent('tool_used', { + toolName: 'ask_codebase', + source: 'sourcebot-mcp-server', + success: true, + }); + const formattedResponse = dedent` ${result.answer} diff --git a/packages/web/src/features/tools/adapters.ts b/packages/web/src/features/tools/adapters.ts index 5b2ac3809..6c9419702 100644 --- a/packages/web/src/features/tools/adapters.ts +++ b/packages/web/src/features/tools/adapters.ts @@ -1,6 +1,7 @@ import { tool } from "ai"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; +import { captureEvent } from "@/lib/posthog"; import { ToolContext, ToolDefinition } from "./types"; export function toVercelAITool( @@ -11,7 +12,21 @@ export function toVercelAITool def.execute(input, context), + execute: async (input) => { + let success = true; + try { + return await def.execute(input, context); + } catch (error) { + success = false; + throw error; + } finally { + captureEvent('tool_used', { + toolName: def.name, + source: context.source ?? 'unknown', + success, + }); + } + }, toModelOutput: ({ output }) => ({ type: "content", value: [{ type: "text", text: output.output }], @@ -41,8 +56,18 @@ export function registerMcpTool Date: Mon, 13 Apr 2026 16:59:37 -0700 Subject: [PATCH 02/14] docs: add CHANGELOG entry for PostHog event changes Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cd9d7619..443f382d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `thinkingLevel` and `thinkingBudget` configuration options for Google Generative AI and Google Vertex providers. [#1110](https://github.com/sourcebot-dev/sourcebot/pull/1110) +- Added unified `tool_used` PostHog event tracking for all tool calls across the ask agent and MCP server. [#1111](https://github.com/sourcebot-dev/sourcebot/pull/1111) +- Added client-side `wa_ask_thread_created` and `wa_ask_message_sent` PostHog events for web-only ask usage tracking. [#1111](https://github.com/sourcebot-dev/sourcebot/pull/1111) ### Changed - Deprecated `GOOGLE_VERTEX_THINKING_BUDGET_TOKENS` environment variable in favor of per-model `thinkingBudget` config. [#1110](https://github.com/sourcebot-dev/sourcebot/pull/1110) - Removed `GOOGLE_VERTEX_INCLUDE_THOUGHTS` environment variable. Thoughts are now always included. [#1110](https://github.com/sourcebot-dev/sourcebot/pull/1110) +- Renamed PostHog events: `wa_chat_thread_created` → `ask_thread_created`, `wa_chat_message_sent` → `ask_message_sent`, `wa_chat_tool_used` → `tool_used`. [#1111](https://github.com/sourcebot-dev/sourcebot/pull/1111) ## [4.16.8] - 2026-04-09 From df039d020c80a7376666b2ca66a2cda8294f1852 Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 13 Apr 2026 17:03:11 -0700 Subject: [PATCH 03/14] feat(web): add missing properties to client-side ask events Add isAnonymous to wa_ask_thread_created and messageCount to wa_ask_message_sent so they carry the same data as the server-side events. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/src/features/chat/components/chatThread/chatThread.tsx | 1 + packages/web/src/features/chat/useCreateNewChatThread.ts | 1 + packages/web/src/lib/posthogEvents.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/packages/web/src/features/chat/components/chatThread/chatThread.tsx b/packages/web/src/features/chat/components/chatThread/chatThread.tsx index b7f8e1447..43af151d5 100644 --- a/packages/web/src/features/chat/components/chatThread/chatThread.tsx +++ b/packages/web/src/features/chat/components/chatThread/chatThread.tsx @@ -128,6 +128,7 @@ export const ChatThread = ({ captureEvent('wa_ask_message_sent', { chatId, + messageCount: messages.length + 1, selectedReposCount: selectedSearchScopes.length, }); diff --git a/packages/web/src/features/chat/useCreateNewChatThread.ts b/packages/web/src/features/chat/useCreateNewChatThread.ts index 951f03c84..56c4d5863 100644 --- a/packages/web/src/features/chat/useCreateNewChatThread.ts +++ b/packages/web/src/features/chat/useCreateNewChatThread.ts @@ -48,6 +48,7 @@ export const useCreateNewChatThread = ({ isAuthenticated = false }: UseCreateNew captureEvent('wa_ask_thread_created', { chatId: response.id, + isAnonymous: response.isAnonymous, }); setChatState({ diff --git a/packages/web/src/lib/posthogEvents.ts b/packages/web/src/lib/posthogEvents.ts index 2a9786186..19967cfd8 100644 --- a/packages/web/src/lib/posthogEvents.ts +++ b/packages/web/src/lib/posthogEvents.ts @@ -161,9 +161,11 @@ export type PosthogEventMap = { }, wa_ask_thread_created: { chatId: string, + isAnonymous: boolean, }, wa_ask_message_sent: { chatId: string, + messageCount: number, selectedReposCount: number, }, ask_thread_created: { From 277ed6053a4a093f6a4da1bd34705052f397d6fa Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 13 Apr 2026 17:16:27 -0700 Subject: [PATCH 04/14] refactor(web): remove redundant api_code_search_request event This event is fully covered by api_request which fires for every API call and carries path + source. Updated the Any Feature Usage PostHog action to remove the api_code_search_request step. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/app/api/(server)/search/route.ts | 7 ------- packages/web/src/app/api/(server)/stream_search/route.ts | 7 ------- packages/web/src/lib/posthogEvents.ts | 4 ---- 3 files changed, 18 deletions(-) diff --git a/packages/web/src/app/api/(server)/search/route.ts b/packages/web/src/app/api/(server)/search/route.ts index d13aae3ba..e15184297 100644 --- a/packages/web/src/app/api/(server)/search/route.ts +++ b/packages/web/src/app/api/(server)/search/route.ts @@ -2,7 +2,6 @@ import { search, searchRequestSchema } from "@/features/search"; import { apiHandler } from "@/lib/apiHandler"; -import { captureEvent } from "@/lib/posthog"; import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; import { NextRequest } from "next/server"; @@ -21,12 +20,6 @@ export const POST = apiHandler(async (request: NextRequest) => { ...options } = parsed.data; - const source = request.headers.get('X-Sourcebot-Client-Source') ?? 'unknown'; - await captureEvent('api_code_search_request', { - source, - type: 'blocking', - }); - const response = await search({ queryType: 'string', query, diff --git a/packages/web/src/app/api/(server)/stream_search/route.ts b/packages/web/src/app/api/(server)/stream_search/route.ts index 4caeba5b4..5c7ab92cb 100644 --- a/packages/web/src/app/api/(server)/stream_search/route.ts +++ b/packages/web/src/app/api/(server)/stream_search/route.ts @@ -2,7 +2,6 @@ import { streamSearch, searchRequestSchema } from '@/features/search'; import { apiHandler } from '@/lib/apiHandler'; -import { captureEvent } from '@/lib/posthog'; import { requestBodySchemaValidationError, serviceErrorResponse } from '@/lib/serviceError'; import { isServiceError } from '@/lib/utils'; import { NextRequest } from 'next/server'; @@ -20,12 +19,6 @@ export const POST = apiHandler(async (request: NextRequest) => { ...options } = parsed.data; - const source = request.headers.get('X-Sourcebot-Client-Source') ?? 'unknown'; - await captureEvent('api_code_search_request', { - source, - type: 'streamed', - }); - const stream = await streamSearch({ queryType: 'string', query, diff --git a/packages/web/src/lib/posthogEvents.ts b/packages/web/src/lib/posthogEvents.ts index 19967cfd8..46da42095 100644 --- a/packages/web/src/lib/posthogEvents.ts +++ b/packages/web/src/lib/posthogEvents.ts @@ -297,10 +297,6 @@ export type PosthogEventMap = { ////////////////////////////////////////////////////////////////// wa_repo_not_found_for_zoekt_file: {}, ////////////////////////////////////////////////////////////////// - api_code_search_request: { - source: string; - type: 'streamed' | 'blocking'; - }, api_request: { path: string; source: string; From 818f237e23c86ed9a96d3eaf61da9a14cd23b518 Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 13 Apr 2026 17:18:45 -0700 Subject: [PATCH 05/14] refactor(web): remove redundant wa_ask_thread_created event Redundant with ask_thread_created which fires server-side for the same thread creation (including from the web UI via createChat server action). Dashboards track ask usage via wa_ask_message_sent, not thread creation. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/features/chat/useCreateNewChatThread.ts | 5 ----- packages/web/src/lib/posthogEvents.ts | 4 ---- 2 files changed, 9 deletions(-) diff --git a/packages/web/src/features/chat/useCreateNewChatThread.ts b/packages/web/src/features/chat/useCreateNewChatThread.ts index 56c4d5863..63ead0249 100644 --- a/packages/web/src/features/chat/useCreateNewChatThread.ts +++ b/packages/web/src/features/chat/useCreateNewChatThread.ts @@ -46,11 +46,6 @@ export const useCreateNewChatThread = ({ isAuthenticated = false }: UseCreateNew return; } - captureEvent('wa_ask_thread_created', { - chatId: response.id, - isAnonymous: response.isAnonymous, - }); - setChatState({ inputMessage, selectedSearchScopes, diff --git a/packages/web/src/lib/posthogEvents.ts b/packages/web/src/lib/posthogEvents.ts index 46da42095..a53bee0ee 100644 --- a/packages/web/src/lib/posthogEvents.ts +++ b/packages/web/src/lib/posthogEvents.ts @@ -159,10 +159,6 @@ export type PosthogEventMap = { chatId: string, messageId: string, }, - wa_ask_thread_created: { - chatId: string, - isAnonymous: boolean, - }, wa_ask_message_sent: { chatId: string, messageCount: number, From 49a1b768235799b76ed2ecbfcc3deec281636e16 Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 13 Apr 2026 17:25:14 -0700 Subject: [PATCH 06/14] refactor(web): remove wa_ask_message_sent, set source header on chat transport Instead of a separate client-side event, the web client now sets the X-Sourcebot-Client-Source header on DefaultChatTransport. This makes ask_message_sent fire with source='sourcebot-web-client' for web UI calls, enabling web-only filtering directly on the server-side event. Updated Ask Usage and Any Feature Usage PostHog actions to filter ask_message_sent by source=sourcebot-web-client. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 2 +- .../features/chat/components/chatThread/chatThread.tsx | 9 +++------ packages/web/src/lib/posthogEvents.ts | 5 ----- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 443f382d3..d9c82cecb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `thinkingLevel` and `thinkingBudget` configuration options for Google Generative AI and Google Vertex providers. [#1110](https://github.com/sourcebot-dev/sourcebot/pull/1110) - Added unified `tool_used` PostHog event tracking for all tool calls across the ask agent and MCP server. [#1111](https://github.com/sourcebot-dev/sourcebot/pull/1111) -- Added client-side `wa_ask_thread_created` and `wa_ask_message_sent` PostHog events for web-only ask usage tracking. [#1111](https://github.com/sourcebot-dev/sourcebot/pull/1111) +- Web app now sets `X-Sourcebot-Client-Source: sourcebot-web-client` header on chat API requests, enabling web-only ask usage filtering in dashboards. [#1111](https://github.com/sourcebot-dev/sourcebot/pull/1111) ### Changed - Deprecated `GOOGLE_VERTEX_THINKING_BUDGET_TOKENS` environment variable in favor of per-model `thinkingBudget` config. [#1110](https://github.com/sourcebot-dev/sourcebot/pull/1110) diff --git a/packages/web/src/features/chat/components/chatThread/chatThread.tsx b/packages/web/src/features/chat/components/chatThread/chatThread.tsx index 43af151d5..f60d281b7 100644 --- a/packages/web/src/features/chat/components/chatThread/chatThread.tsx +++ b/packages/web/src/features/chat/components/chatThread/chatThread.tsx @@ -102,6 +102,9 @@ export const ChatThread = ({ messages: initialMessages, transport: new DefaultChatTransport({ api: '/api/chat', + headers: { + 'X-Sourcebot-Client-Source': 'sourcebot-web-client', + }, }), onData: (dataPart) => { // Keeps sources added by the assistant in sync. @@ -126,12 +129,6 @@ export const ChatThread = ({ .map((part) => part.data); setSources((prev) => [...prev, ...sources]); - captureEvent('wa_ask_message_sent', { - chatId, - messageCount: messages.length + 1, - selectedReposCount: selectedSearchScopes.length, - }); - _sendMessage(message, { body: { selectedSearchScopes, diff --git a/packages/web/src/lib/posthogEvents.ts b/packages/web/src/lib/posthogEvents.ts index a53bee0ee..94d3d9b88 100644 --- a/packages/web/src/lib/posthogEvents.ts +++ b/packages/web/src/lib/posthogEvents.ts @@ -159,11 +159,6 @@ export type PosthogEventMap = { chatId: string, messageId: string, }, - wa_ask_message_sent: { - chatId: string, - messageCount: number, - selectedReposCount: number, - }, ask_thread_created: { chatId: string, isAnonymous: boolean, From 094cfe1e20f26047775522174a2c04e87a8221c9 Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 13 Apr 2026 17:38:16 -0700 Subject: [PATCH 07/14] docs: consolidate CHANGELOG entries for #1111 into single entry Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9c82cecb..e105f22da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `thinkingLevel` and `thinkingBudget` configuration options for Google Generative AI and Google Vertex providers. [#1110](https://github.com/sourcebot-dev/sourcebot/pull/1110) -- Added unified `tool_used` PostHog event tracking for all tool calls across the ask agent and MCP server. [#1111](https://github.com/sourcebot-dev/sourcebot/pull/1111) -- Web app now sets `X-Sourcebot-Client-Source: sourcebot-web-client` header on chat API requests, enabling web-only ask usage filtering in dashboards. [#1111](https://github.com/sourcebot-dev/sourcebot/pull/1111) ### Changed - Deprecated `GOOGLE_VERTEX_THINKING_BUDGET_TOKENS` environment variable in favor of per-model `thinkingBudget` config. [#1110](https://github.com/sourcebot-dev/sourcebot/pull/1110) - Removed `GOOGLE_VERTEX_INCLUDE_THOUGHTS` environment variable. Thoughts are now always included. [#1110](https://github.com/sourcebot-dev/sourcebot/pull/1110) -- Renamed PostHog events: `wa_chat_thread_created` → `ask_thread_created`, `wa_chat_message_sent` → `ask_message_sent`, `wa_chat_tool_used` → `tool_used`. [#1111](https://github.com/sourcebot-dev/sourcebot/pull/1111) +- Renamed and consolidated PostHog chat events (`wa_chat_thread_created` -> `ask_thread_created`, `wa_chat_message_sent` -> `ask_message_sent`, `wa_chat_tool_used` -> `tool_used`), added unified `tool_used` tracking across the ask agent and MCP server, and removed the redundant `api_code_search_request` event. [#1111](https://github.com/sourcebot-dev/sourcebot/pull/1111) ## [4.16.8] - 2026-04-09 From 39aaac0b4fb95ceedab03663882ddf8403a25197 Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 13 Apr 2026 17:39:21 -0700 Subject: [PATCH 08/14] fix(web): handle unhandled promise rejections in tool_used captureEvent calls Add .catch(() => {}) to all fire-and-forget captureEvent('tool_used') calls in adapters.ts and server.ts, matching the existing pattern in apiHandler.ts. Prevents unhandled promise rejections if telemetry fails. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/features/mcp/server.ts | 9 ++++++++- packages/web/src/features/tools/adapters.ts | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/web/src/features/mcp/server.ts b/packages/web/src/features/mcp/server.ts index ec4103f36..d6ba9102e 100644 --- a/packages/web/src/features/mcp/server.ts +++ b/packages/web/src/features/mcp/server.ts @@ -6,7 +6,7 @@ import { captureEvent } from '@/lib/posthog'; import { isServiceError } from '@/lib/utils'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { ChatVisibility } from '@sourcebot/db'; -import { SOURCEBOT_VERSION } from '@sourcebot/shared'; +import { createLogger, SOURCEBOT_VERSION } from '@sourcebot/shared'; import _dedent from 'dedent'; import { z } from 'zod'; import { getConfiguredLanguageModelsInfo } from "../chat/utils.server"; @@ -24,6 +24,7 @@ import { } from '../tools'; const dedent = _dedent.withOptions({ alignValues: true }); +const logger = createLogger('mcp-server'); export async function createMcpServer(): Promise { const server = new McpServer({ @@ -62,6 +63,8 @@ export async function createMcpServer(): Promise { toolName: 'list_language_models', source: 'sourcebot-mcp-server', success: true, + }).catch((error) => { + logger.warn('Failed to capture tool_used event:', error); }); return { content: [{ type: "text", text: JSON.stringify(models) }] }; } @@ -111,6 +114,8 @@ export async function createMcpServer(): Promise { toolName: 'ask_codebase', source: 'sourcebot-mcp-server', success: false, + }).catch((error) => { + logger.warn('Failed to capture tool_used event:', error); }); return { content: [{ type: "text", text: `Failed to ask codebase: ${result.message}` }], @@ -121,6 +126,8 @@ export async function createMcpServer(): Promise { toolName: 'ask_codebase', source: 'sourcebot-mcp-server', success: true, + }).catch((error) => { + logger.warn('Failed to capture tool_used event:', error); }); const formattedResponse = dedent` diff --git a/packages/web/src/features/tools/adapters.ts b/packages/web/src/features/tools/adapters.ts index 6c9419702..0aacf9342 100644 --- a/packages/web/src/features/tools/adapters.ts +++ b/packages/web/src/features/tools/adapters.ts @@ -3,6 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { captureEvent } from "@/lib/posthog"; import { ToolContext, ToolDefinition } from "./types"; +import { logger } from "./logger"; export function toVercelAITool( def: ToolDefinition, @@ -24,6 +25,8 @@ export function toVercelAITool { + logger.warn(`Failed to capture tool_used event for ${def.name}:`, error); }); } }, @@ -60,6 +63,8 @@ export function registerMcpTool { + logger.warn(`Failed to capture tool_used event for ${def.name}:`, error); }); return { content: [{ type: "text" as const, text: result.output }] }; } catch (error) { @@ -67,6 +72,8 @@ export function registerMcpTool { + logger.warn(`Failed to capture tool_used event for ${def.name}:`, error); }); const message = error instanceof Error ? error.message : String(error); return { content: [{ type: "text" as const, text: `Tool "${def.name}" failed: ${message}` }], isError: true }; From df7c29859c8875de0c81d689188f0dd075a355ae Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 13 Apr 2026 18:00:29 -0700 Subject: [PATCH 09/14] fix(web): fix inflated MCP DAU by threading user identity through tool telemetry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit captureEvent() relies on tryGetPostHogDistinctId() which reads cookies, session, or API key headers. In the MCP server and ask agent contexts, none of these are available, so every tool_used event got a random distinct_id — inflating DAU counts (e.g., 8 tool calls = 8 "users"). Fix: add optional distinctId to ToolContext and captureEvent(). Thread user.id from the auth context through: - MCP route → createMcpServer(userId) → ToolContext.distinctId - chat route / askCodebase → createMessageStream(distinctId) → createAgentStream → createTools Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/app/api/(server)/chat/route.ts | 1 + packages/web/src/app/api/(server)/mcp/route.ts | 2 +- packages/web/src/features/chat/agent.ts | 8 +++++++- packages/web/src/features/mcp/askCodebase.ts | 1 + packages/web/src/features/mcp/server.ts | 9 +++++---- packages/web/src/features/tools/adapters.ts | 6 +++--- packages/web/src/features/tools/types.ts | 2 ++ packages/web/src/lib/posthog.ts | 4 ++-- 8 files changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/web/src/app/api/(server)/chat/route.ts b/packages/web/src/app/api/(server)/chat/route.ts index 4c0b12819..351e0779a 100644 --- a/packages/web/src/app/api/(server)/chat/route.ts +++ b/packages/web/src/app/api/(server)/chat/route.ts @@ -112,6 +112,7 @@ export const POST = apiHandler(async (req: NextRequest) => { modelName: languageModelConfig.displayName ?? languageModelConfig.model, modelProviderOptions: providerOptions, modelTemperature: temperature, + distinctId: user?.id, onFinish: async ({ messages }) => { await updateChatMessages({ chatId: id, messages, prisma }); }, diff --git a/packages/web/src/app/api/(server)/mcp/route.ts b/packages/web/src/app/api/(server)/mcp/route.ts index a4225e20e..3a717e479 100644 --- a/packages/web/src/app/api/(server)/mcp/route.ts +++ b/packages/web/src/app/api/(server)/mcp/route.ts @@ -79,7 +79,7 @@ export const POST = apiHandler(async (request: NextRequest) => { }, }); - const mcpServer = await createMcpServer(); + const mcpServer = await createMcpServer({ userId: user?.id }); await mcpServer.connect(transport); return transport.handleRequest(request); diff --git a/packages/web/src/features/chat/agent.ts b/packages/web/src/features/chat/agent.ts index 784ca5344..cbf6c6f86 100644 --- a/packages/web/src/features/chat/agent.ts +++ b/packages/web/src/features/chat/agent.ts @@ -43,6 +43,8 @@ interface CreateMessageStreamResponseProps { modelProviderOptions?: Record>; modelTemperature?: number; metadata?: Partial; + /** PostHog distinct ID for telemetry attribution on tool_used events. */ + distinctId?: string; } export const createMessageStream = async ({ @@ -56,6 +58,7 @@ export const createMessageStream = async ({ modelTemperature, onFinish, onError, + distinctId, }: CreateMessageStreamResponseProps) => { const latestMessage = messages[messages.length - 1]; const sources = latestMessage.parts @@ -101,6 +104,7 @@ export const createMessageStream = async ({ inputMessages: messageHistory, inputSources: sources, selectedRepos, + distinctId, onWriteSource: (source) => { writer.write({ type: 'data-source', @@ -154,6 +158,7 @@ interface AgentOptions { onWriteSource: (source: Source) => void; traceId: string; chatId: string; + distinctId?: string; } const createAgentStream = async ({ @@ -166,6 +171,7 @@ const createAgentStream = async ({ onWriteSource, traceId, chatId, + distinctId, }: AgentOptions) => { // For every file source, resolve the source code so that we can include it in the system prompt. const fileSources = inputSources.filter((source) => source.type === 'file'); @@ -202,7 +208,7 @@ const createAgentStream = async ({ providerOptions, messages: inputMessages, system: systemPrompt, - tools: createTools({ source: 'sourcebot-ask-agent', selectedRepos }), + tools: createTools({ source: 'sourcebot-ask-agent', selectedRepos, distinctId }), temperature: temperature ?? env.SOURCEBOT_CHAT_MODEL_TEMPERATURE, stopWhen: [ stepCountIsGTE(env.SOURCEBOT_CHAT_MAX_STEP_COUNT), diff --git a/packages/web/src/features/mcp/askCodebase.ts b/packages/web/src/features/mcp/askCodebase.ts index 6451e680f..711228d6d 100644 --- a/packages/web/src/features/mcp/askCodebase.ts +++ b/packages/web/src/features/mcp/askCodebase.ts @@ -159,6 +159,7 @@ export const askCodebase = (params: AskCodebaseParams): Promise { finalMessages = messages; }, diff --git a/packages/web/src/features/mcp/server.ts b/packages/web/src/features/mcp/server.ts index d6ba9102e..601cc1a3f 100644 --- a/packages/web/src/features/mcp/server.ts +++ b/packages/web/src/features/mcp/server.ts @@ -26,7 +26,7 @@ import { const dedent = _dedent.withOptions({ alignValues: true }); const logger = createLogger('mcp-server'); -export async function createMcpServer(): Promise { +export async function createMcpServer(options?: { userId?: string }): Promise { const server = new McpServer({ name: 'sourcebot-mcp-server', version: SOURCEBOT_VERSION, @@ -37,6 +37,7 @@ export async function createMcpServer(): Promise { const toolContext: ToolContext = { source: 'sourcebot-mcp-server', + distinctId: options?.userId, } registerMcpTool(server, grepDefinition, toolContext); @@ -63,7 +64,7 @@ export async function createMcpServer(): Promise { toolName: 'list_language_models', source: 'sourcebot-mcp-server', success: true, - }).catch((error) => { + }, { distinctId: options?.userId }).catch((error) => { logger.warn('Failed to capture tool_used event:', error); }); return { content: [{ type: "text", text: JSON.stringify(models) }] }; @@ -114,7 +115,7 @@ export async function createMcpServer(): Promise { toolName: 'ask_codebase', source: 'sourcebot-mcp-server', success: false, - }).catch((error) => { + }, { distinctId: options?.userId }).catch((error) => { logger.warn('Failed to capture tool_used event:', error); }); return { @@ -126,7 +127,7 @@ export async function createMcpServer(): Promise { toolName: 'ask_codebase', source: 'sourcebot-mcp-server', success: true, - }).catch((error) => { + }, { distinctId: options?.userId }).catch((error) => { logger.warn('Failed to capture tool_used event:', error); }); diff --git a/packages/web/src/features/tools/adapters.ts b/packages/web/src/features/tools/adapters.ts index 0aacf9342..3274c8275 100644 --- a/packages/web/src/features/tools/adapters.ts +++ b/packages/web/src/features/tools/adapters.ts @@ -25,7 +25,7 @@ export function toVercelAITool { + }, { distinctId: context.distinctId }).catch((error) => { logger.warn(`Failed to capture tool_used event for ${def.name}:`, error); }); } @@ -63,7 +63,7 @@ export function registerMcpTool { + }, { distinctId: context.distinctId }).catch((error) => { logger.warn(`Failed to capture tool_used event for ${def.name}:`, error); }); return { content: [{ type: "text" as const, text: result.output }] }; @@ -72,7 +72,7 @@ export function registerMcpTool { + }, { distinctId: context.distinctId }).catch((error) => { logger.warn(`Failed to capture tool_used event for ${def.name}:`, error); }); const message = error instanceof Error ? error.message : String(error); diff --git a/packages/web/src/features/tools/types.ts b/packages/web/src/features/tools/types.ts index 9c221a409..04d46f775 100644 --- a/packages/web/src/features/tools/types.ts +++ b/packages/web/src/features/tools/types.ts @@ -17,6 +17,8 @@ export type Source = z.infer; export interface ToolContext { source?: string; selectedRepos?: string[]; + /** PostHog distinct ID for telemetry attribution. When set, tool_used events will be attributed to this user. */ + distinctId?: string; } export interface ToolDefinition< diff --git a/packages/web/src/lib/posthog.ts b/packages/web/src/lib/posthog.ts index 33d335fb8..c4cd05f07 100644 --- a/packages/web/src/lib/posthog.ts +++ b/packages/web/src/lib/posthog.ts @@ -87,12 +87,12 @@ export const createPostHogClient = async () => { return posthog; } -export async function captureEvent(event: E, properties: PosthogEventMap[E]) { +export async function captureEvent(event: E, properties: PosthogEventMap[E], options?: { distinctId?: string }) { if (env.SOURCEBOT_TELEMETRY_DISABLED === 'true') { return; } - const distinctId = await tryGetPostHogDistinctId(); + const distinctId = options?.distinctId ?? await tryGetPostHogDistinctId(); const posthog = await createPostHogClient(); const headersList = await headers(); From 241048034f32d65cacbdb68a120e7b687dcb0e47 Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 13 Apr 2026 18:02:57 -0700 Subject: [PATCH 10/14] refactor(web): rename distinctId to userId in ToolContext The field carries a user ID, not a PostHog-specific concept. The captureEvent options parameter remains distinctId since that's the PostHog term, but the application-level interfaces use userId. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/app/api/(server)/chat/route.ts | 2 +- packages/web/src/features/chat/agent.ts | 14 +++++++------- packages/web/src/features/mcp/askCodebase.ts | 2 +- packages/web/src/features/mcp/server.ts | 2 +- packages/web/src/features/tools/adapters.ts | 6 +++--- packages/web/src/features/tools/types.ts | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/web/src/app/api/(server)/chat/route.ts b/packages/web/src/app/api/(server)/chat/route.ts index 351e0779a..0ea5157f6 100644 --- a/packages/web/src/app/api/(server)/chat/route.ts +++ b/packages/web/src/app/api/(server)/chat/route.ts @@ -112,7 +112,7 @@ export const POST = apiHandler(async (req: NextRequest) => { modelName: languageModelConfig.displayName ?? languageModelConfig.model, modelProviderOptions: providerOptions, modelTemperature: temperature, - distinctId: user?.id, + userId: user?.id, onFinish: async ({ messages }) => { await updateChatMessages({ chatId: id, messages, prisma }); }, diff --git a/packages/web/src/features/chat/agent.ts b/packages/web/src/features/chat/agent.ts index cbf6c6f86..85541d4c1 100644 --- a/packages/web/src/features/chat/agent.ts +++ b/packages/web/src/features/chat/agent.ts @@ -43,8 +43,8 @@ interface CreateMessageStreamResponseProps { modelProviderOptions?: Record>; modelTemperature?: number; metadata?: Partial; - /** PostHog distinct ID for telemetry attribution on tool_used events. */ - distinctId?: string; + /** User ID for telemetry attribution on tool_used events. */ + userId?: string; } export const createMessageStream = async ({ @@ -58,7 +58,7 @@ export const createMessageStream = async ({ modelTemperature, onFinish, onError, - distinctId, + userId, }: CreateMessageStreamResponseProps) => { const latestMessage = messages[messages.length - 1]; const sources = latestMessage.parts @@ -104,7 +104,7 @@ export const createMessageStream = async ({ inputMessages: messageHistory, inputSources: sources, selectedRepos, - distinctId, + userId, onWriteSource: (source) => { writer.write({ type: 'data-source', @@ -158,7 +158,7 @@ interface AgentOptions { onWriteSource: (source: Source) => void; traceId: string; chatId: string; - distinctId?: string; + userId?: string; } const createAgentStream = async ({ @@ -171,7 +171,7 @@ const createAgentStream = async ({ onWriteSource, traceId, chatId, - distinctId, + userId, }: AgentOptions) => { // For every file source, resolve the source code so that we can include it in the system prompt. const fileSources = inputSources.filter((source) => source.type === 'file'); @@ -208,7 +208,7 @@ const createAgentStream = async ({ providerOptions, messages: inputMessages, system: systemPrompt, - tools: createTools({ source: 'sourcebot-ask-agent', selectedRepos, distinctId }), + tools: createTools({ source: 'sourcebot-ask-agent', selectedRepos, userId }), temperature: temperature ?? env.SOURCEBOT_CHAT_MODEL_TEMPERATURE, stopWhen: [ stepCountIsGTE(env.SOURCEBOT_CHAT_MAX_STEP_COUNT), diff --git a/packages/web/src/features/mcp/askCodebase.ts b/packages/web/src/features/mcp/askCodebase.ts index 711228d6d..7a6660f47 100644 --- a/packages/web/src/features/mcp/askCodebase.ts +++ b/packages/web/src/features/mcp/askCodebase.ts @@ -159,7 +159,7 @@ export const askCodebase = (params: AskCodebaseParams): Promise { finalMessages = messages; }, diff --git a/packages/web/src/features/mcp/server.ts b/packages/web/src/features/mcp/server.ts index 601cc1a3f..13f932ad7 100644 --- a/packages/web/src/features/mcp/server.ts +++ b/packages/web/src/features/mcp/server.ts @@ -37,7 +37,7 @@ export async function createMcpServer(options?: { userId?: string }): Promise { + }, { distinctId: context.userId }).catch((error) => { logger.warn(`Failed to capture tool_used event for ${def.name}:`, error); }); } @@ -63,7 +63,7 @@ export function registerMcpTool { + }, { distinctId: context.userId }).catch((error) => { logger.warn(`Failed to capture tool_used event for ${def.name}:`, error); }); return { content: [{ type: "text" as const, text: result.output }] }; @@ -72,7 +72,7 @@ export function registerMcpTool { + }, { distinctId: context.userId }).catch((error) => { logger.warn(`Failed to capture tool_used event for ${def.name}:`, error); }); const message = error instanceof Error ? error.message : String(error); diff --git a/packages/web/src/features/tools/types.ts b/packages/web/src/features/tools/types.ts index 04d46f775..ad3dd3cfc 100644 --- a/packages/web/src/features/tools/types.ts +++ b/packages/web/src/features/tools/types.ts @@ -17,8 +17,8 @@ export type Source = z.infer; export interface ToolContext { source?: string; selectedRepos?: string[]; - /** PostHog distinct ID for telemetry attribution. When set, tool_used events will be attributed to this user. */ - distinctId?: string; + /** User ID for telemetry attribution. When set, tool_used events will be attributed to this user. */ + userId?: string; } export interface ToolDefinition< From 5bfdd29a4c7f32a1458ce4c64e02e9ef51458369 Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 13 Apr 2026 18:03:30 -0700 Subject: [PATCH 11/14] refactor(web): use try/finally pattern in registerMcpTool for tool_used event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the same pattern used in toVercelAITool — single captureEvent call in a finally block with a success boolean, instead of duplicated calls in try and catch. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/features/tools/adapters.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/web/src/features/tools/adapters.ts b/packages/web/src/features/tools/adapters.ts index 61c3511a0..44cb36cd3 100644 --- a/packages/web/src/features/tools/adapters.ts +++ b/packages/web/src/features/tools/adapters.ts @@ -56,27 +56,23 @@ export function registerMcpTool { + let success = true; try { const parsed = def.inputSchema.parse(input); const result = await def.execute(parsed, context); - captureEvent('tool_used', { - toolName: def.name, - source: context.source ?? 'unknown', - success: true, - }, { distinctId: context.userId }).catch((error) => { - logger.warn(`Failed to capture tool_used event for ${def.name}:`, error); - }); return { content: [{ type: "text" as const, text: result.output }] }; } catch (error) { + success = false; + const message = error instanceof Error ? error.message : String(error); + return { content: [{ type: "text" as const, text: `Tool "${def.name}" failed: ${message}` }], isError: true }; + } finally { captureEvent('tool_used', { toolName: def.name, source: context.source ?? 'unknown', - success: false, + success, }, { distinctId: context.userId }).catch((error) => { logger.warn(`Failed to capture tool_used event for ${def.name}:`, error); }); - const message = error instanceof Error ? error.message : String(error); - return { content: [{ type: "text" as const, text: `Tool "${def.name}" failed: ${message}` }], isError: true }; } }, ); From 64bc4b69d65caf80681ecfc0d996cd2335a9384d Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 13 Apr 2026 18:05:04 -0700 Subject: [PATCH 12/14] refactor(web): move captureEvent error handling into captureEvent itself captureEvent now wraps its body in try/catch so callers are guaranteed it won't throw. Removed all .catch() handlers from call sites in adapters.ts, server.ts, and apiHandler.ts. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/features/mcp/server.ts | 15 ++------ packages/web/src/features/tools/adapters.ts | 9 +---- packages/web/src/lib/apiHandler.ts | 4 +- packages/web/src/lib/posthog.ts | 42 +++++++++++---------- 4 files changed, 30 insertions(+), 40 deletions(-) diff --git a/packages/web/src/features/mcp/server.ts b/packages/web/src/features/mcp/server.ts index 13f932ad7..7ad4a6a81 100644 --- a/packages/web/src/features/mcp/server.ts +++ b/packages/web/src/features/mcp/server.ts @@ -6,7 +6,7 @@ import { captureEvent } from '@/lib/posthog'; import { isServiceError } from '@/lib/utils'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { ChatVisibility } from '@sourcebot/db'; -import { createLogger, SOURCEBOT_VERSION } from '@sourcebot/shared'; +import { SOURCEBOT_VERSION } from '@sourcebot/shared'; import _dedent from 'dedent'; import { z } from 'zod'; import { getConfiguredLanguageModelsInfo } from "../chat/utils.server"; @@ -24,7 +24,6 @@ import { } from '../tools'; const dedent = _dedent.withOptions({ alignValues: true }); -const logger = createLogger('mcp-server'); export async function createMcpServer(options?: { userId?: string }): Promise { const server = new McpServer({ @@ -64,9 +63,7 @@ export async function createMcpServer(options?: { userId?: string }): Promise { - logger.warn('Failed to capture tool_used event:', error); - }); + }, { distinctId: options?.userId }); return { content: [{ type: "text", text: JSON.stringify(models) }] }; } ); @@ -115,9 +112,7 @@ export async function createMcpServer(options?: { userId?: string }): Promise { - logger.warn('Failed to capture tool_used event:', error); - }); + }, { distinctId: options?.userId }); return { content: [{ type: "text", text: `Failed to ask codebase: ${result.message}` }], }; @@ -127,9 +122,7 @@ export async function createMcpServer(options?: { userId?: string }): Promise { - logger.warn('Failed to capture tool_used event:', error); - }); + }, { distinctId: options?.userId }); const formattedResponse = dedent` ${result.answer} diff --git a/packages/web/src/features/tools/adapters.ts b/packages/web/src/features/tools/adapters.ts index 44cb36cd3..6eca293f1 100644 --- a/packages/web/src/features/tools/adapters.ts +++ b/packages/web/src/features/tools/adapters.ts @@ -3,7 +3,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { captureEvent } from "@/lib/posthog"; import { ToolContext, ToolDefinition } from "./types"; -import { logger } from "./logger"; export function toVercelAITool( def: ToolDefinition, @@ -25,9 +24,7 @@ export function toVercelAITool { - logger.warn(`Failed to capture tool_used event for ${def.name}:`, error); - }); + }, { distinctId: context.userId }); } }, toModelOutput: ({ output }) => ({ @@ -70,9 +67,7 @@ export function registerMcpTool { - logger.warn(`Failed to capture tool_used event for ${def.name}:`, error); - }); + }, { distinctId: context.userId }); } }, ); diff --git a/packages/web/src/lib/apiHandler.ts b/packages/web/src/lib/apiHandler.ts index 65c76f116..0015a805a 100644 --- a/packages/web/src/lib/apiHandler.ts +++ b/packages/web/src/lib/apiHandler.ts @@ -47,9 +47,7 @@ export function apiHandler( const source = request.headers.get('X-Sourcebot-Client-Source') ?? 'unknown'; // Fire and forget - don't await to avoid blocking the request - captureEvent('api_request', { path, method, source }).catch(() => { - // Silently ignore tracking errors - }); + captureEvent('api_request', { path, method, source }); } // Call the original handler with all arguments diff --git a/packages/web/src/lib/posthog.ts b/packages/web/src/lib/posthog.ts index c4cd05f07..d0011a588 100644 --- a/packages/web/src/lib/posthog.ts +++ b/packages/web/src/lib/posthog.ts @@ -88,24 +88,28 @@ export const createPostHogClient = async () => { } export async function captureEvent(event: E, properties: PosthogEventMap[E], options?: { distinctId?: string }) { - if (env.SOURCEBOT_TELEMETRY_DISABLED === 'true') { - return; - } - - const distinctId = options?.distinctId ?? await tryGetPostHogDistinctId(); - const posthog = await createPostHogClient(); + try { + if (env.SOURCEBOT_TELEMETRY_DISABLED === 'true') { + return; + } - const headersList = await headers(); - const host = headersList.get("host") ?? undefined; - - posthog.capture({ - event, - properties: { - ...properties, - sourcebot_version: SOURCEBOT_VERSION, - install_id: env.SOURCEBOT_INSTALL_ID, - $host: host, - }, - distinctId, - }); + const distinctId = options?.distinctId ?? await tryGetPostHogDistinctId(); + const posthog = await createPostHogClient(); + + const headersList = await headers(); + const host = headersList.get("host") ?? undefined; + + posthog.capture({ + event, + properties: { + ...properties, + sourcebot_version: SOURCEBOT_VERSION, + install_id: env.SOURCEBOT_INSTALL_ID, + $host: host, + }, + distinctId, + }); + } catch { + // Telemetry should never break application functionality. + } } \ No newline at end of file From bb091c0c357d6f1015c0766d7312c66de0ffc1e3 Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 13 Apr 2026 18:09:51 -0700 Subject: [PATCH 13/14] refactor(web): fix tryGetPostHogDistinctId to use getAuthenticatedUser tryGetPostHogDistinctId() was missing OAuth Bearer token resolution, causing all MCP tool calls to get random distinct_ids (inflated DAU). Fix: replace the manual cookie/session/API key checks with a call to getAuthenticatedUser(), which already handles all auth methods (session, OAuth, API keys). This removes the need to thread userId through ToolContext, agent options, and all callers. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/src/app/api/(server)/chat/route.ts | 1 - .../web/src/app/api/(server)/mcp/route.ts | 2 +- packages/web/src/features/chat/agent.ts | 8 +---- packages/web/src/features/mcp/askCodebase.ts | 1 - packages/web/src/features/mcp/server.ts | 9 +++--- packages/web/src/features/tools/adapters.ts | 4 +-- packages/web/src/features/tools/types.ts | 2 -- packages/web/src/lib/posthog.ts | 30 +++++++------------ 8 files changed, 18 insertions(+), 39 deletions(-) diff --git a/packages/web/src/app/api/(server)/chat/route.ts b/packages/web/src/app/api/(server)/chat/route.ts index 0ea5157f6..4c0b12819 100644 --- a/packages/web/src/app/api/(server)/chat/route.ts +++ b/packages/web/src/app/api/(server)/chat/route.ts @@ -112,7 +112,6 @@ export const POST = apiHandler(async (req: NextRequest) => { modelName: languageModelConfig.displayName ?? languageModelConfig.model, modelProviderOptions: providerOptions, modelTemperature: temperature, - userId: user?.id, onFinish: async ({ messages }) => { await updateChatMessages({ chatId: id, messages, prisma }); }, diff --git a/packages/web/src/app/api/(server)/mcp/route.ts b/packages/web/src/app/api/(server)/mcp/route.ts index 3a717e479..a4225e20e 100644 --- a/packages/web/src/app/api/(server)/mcp/route.ts +++ b/packages/web/src/app/api/(server)/mcp/route.ts @@ -79,7 +79,7 @@ export const POST = apiHandler(async (request: NextRequest) => { }, }); - const mcpServer = await createMcpServer({ userId: user?.id }); + const mcpServer = await createMcpServer(); await mcpServer.connect(transport); return transport.handleRequest(request); diff --git a/packages/web/src/features/chat/agent.ts b/packages/web/src/features/chat/agent.ts index 85541d4c1..784ca5344 100644 --- a/packages/web/src/features/chat/agent.ts +++ b/packages/web/src/features/chat/agent.ts @@ -43,8 +43,6 @@ interface CreateMessageStreamResponseProps { modelProviderOptions?: Record>; modelTemperature?: number; metadata?: Partial; - /** User ID for telemetry attribution on tool_used events. */ - userId?: string; } export const createMessageStream = async ({ @@ -58,7 +56,6 @@ export const createMessageStream = async ({ modelTemperature, onFinish, onError, - userId, }: CreateMessageStreamResponseProps) => { const latestMessage = messages[messages.length - 1]; const sources = latestMessage.parts @@ -104,7 +101,6 @@ export const createMessageStream = async ({ inputMessages: messageHistory, inputSources: sources, selectedRepos, - userId, onWriteSource: (source) => { writer.write({ type: 'data-source', @@ -158,7 +154,6 @@ interface AgentOptions { onWriteSource: (source: Source) => void; traceId: string; chatId: string; - userId?: string; } const createAgentStream = async ({ @@ -171,7 +166,6 @@ const createAgentStream = async ({ onWriteSource, traceId, chatId, - userId, }: AgentOptions) => { // For every file source, resolve the source code so that we can include it in the system prompt. const fileSources = inputSources.filter((source) => source.type === 'file'); @@ -208,7 +202,7 @@ const createAgentStream = async ({ providerOptions, messages: inputMessages, system: systemPrompt, - tools: createTools({ source: 'sourcebot-ask-agent', selectedRepos, userId }), + tools: createTools({ source: 'sourcebot-ask-agent', selectedRepos }), temperature: temperature ?? env.SOURCEBOT_CHAT_MODEL_TEMPERATURE, stopWhen: [ stepCountIsGTE(env.SOURCEBOT_CHAT_MAX_STEP_COUNT), diff --git a/packages/web/src/features/mcp/askCodebase.ts b/packages/web/src/features/mcp/askCodebase.ts index 7a6660f47..6451e680f 100644 --- a/packages/web/src/features/mcp/askCodebase.ts +++ b/packages/web/src/features/mcp/askCodebase.ts @@ -159,7 +159,6 @@ export const askCodebase = (params: AskCodebaseParams): Promise { finalMessages = messages; }, diff --git a/packages/web/src/features/mcp/server.ts b/packages/web/src/features/mcp/server.ts index 7ad4a6a81..ec4103f36 100644 --- a/packages/web/src/features/mcp/server.ts +++ b/packages/web/src/features/mcp/server.ts @@ -25,7 +25,7 @@ import { const dedent = _dedent.withOptions({ alignValues: true }); -export async function createMcpServer(options?: { userId?: string }): Promise { +export async function createMcpServer(): Promise { const server = new McpServer({ name: 'sourcebot-mcp-server', version: SOURCEBOT_VERSION, @@ -36,7 +36,6 @@ export async function createMcpServer(options?: { userId?: string }): Promise ({ @@ -67,7 +67,7 @@ export function registerMcpTool; export interface ToolContext { source?: string; selectedRepos?: string[]; - /** User ID for telemetry attribution. When set, tool_used events will be attributed to this user. */ - userId?: string; } export interface ToolDefinition< diff --git a/packages/web/src/lib/posthog.ts b/packages/web/src/lib/posthog.ts index d0011a588..d45a4d5f2 100644 --- a/packages/web/src/lib/posthog.ts +++ b/packages/web/src/lib/posthog.ts @@ -4,8 +4,7 @@ import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies'; import * as Sentry from "@sentry/nextjs"; import { PosthogEvent, PosthogEventMap } from './posthogEvents'; import { cookies, headers } from 'next/headers'; -import { auth } from '@/auth'; -import { getVerifiedApiObject } from '@/middleware/withAuth'; +import { getAuthenticatedUser } from '@/middleware/withAuth'; /** * @note: This is a subset of the properties stored in the @@ -53,28 +52,19 @@ const getPostHogCookie = (cookieStore: Pick): PostHogCook * Attempts to retrieve the distinct id of the current user. */ export const tryGetPostHogDistinctId = async () => { - // First, attempt to retrieve the distinct id from the cookie. + // First, attempt to retrieve the distinct id from the PostHog cookie + // (set by the client-side PostHog SDK). This preserves identity + // continuity between client-side and server-side events. const cookieStore = await cookies(); const cookie = getPostHogCookie(cookieStore); if (cookie) { return cookie.distinct_id; } - // Next, from the session. - const session = await auth(); - if (session) { - return session.user.id; - } - - // Finally, from the api key. - const headersList = await headers(); - const apiKeyString = headersList.get("X-Sourcebot-Api-Key") ?? undefined; - if (!apiKeyString) { - return undefined; - } - - const apiKey = await getVerifiedApiObject(apiKeyString); - return apiKey?.createdById; + // Fall back to the authenticated user's ID. This covers all auth + // methods: session cookies, OAuth Bearer tokens, and API keys. + const authResult = await getAuthenticatedUser(); + return authResult?.user.id; } export const createPostHogClient = async () => { @@ -87,13 +77,13 @@ export const createPostHogClient = async () => { return posthog; } -export async function captureEvent(event: E, properties: PosthogEventMap[E], options?: { distinctId?: string }) { +export async function captureEvent(event: E, properties: PosthogEventMap[E]) { try { if (env.SOURCEBOT_TELEMETRY_DISABLED === 'true') { return; } - const distinctId = options?.distinctId ?? await tryGetPostHogDistinctId(); + const distinctId = await tryGetPostHogDistinctId(); const posthog = await createPostHogClient(); const headersList = await headers(); From d5f4e508f9fe88071c9d34997af94ed68c3495e0 Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 13 Apr 2026 18:11:32 -0700 Subject: [PATCH 14/14] fix(web): add error logging in captureEvent catch block Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/lib/posthog.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/web/src/lib/posthog.ts b/packages/web/src/lib/posthog.ts index d45a4d5f2..4c4a56c0d 100644 --- a/packages/web/src/lib/posthog.ts +++ b/packages/web/src/lib/posthog.ts @@ -1,11 +1,13 @@ import { PostHog } from 'posthog-node' -import { env, SOURCEBOT_VERSION } from '@sourcebot/shared' +import { createLogger, env, SOURCEBOT_VERSION } from '@sourcebot/shared' import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies'; import * as Sentry from "@sentry/nextjs"; import { PosthogEvent, PosthogEventMap } from './posthogEvents'; import { cookies, headers } from 'next/headers'; import { getAuthenticatedUser } from '@/middleware/withAuth'; +const logger = createLogger('posthog'); + /** * @note: This is a subset of the properties stored in the * ph_phc__posthog cookie. @@ -99,7 +101,7 @@ export async function captureEvent(event: E, properties: }, distinctId, }); - } catch { - // Telemetry should never break application functionality. + } catch (error) { + logger.error('Failed to capture PostHog event:', error); } } \ No newline at end of file