Skip to content

Commit 5305019

Browse files
msukkariclaude
andauthored
feat(web): rename PostHog chat events, add tool_used tracking, and client-side ask events (#1111)
* 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) <noreply@anthropic.com> * docs: add CHANGELOG entry for PostHog event changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * docs: consolidate CHANGELOG entries for #1111 into single entry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * fix(web): fix inflated MCP DAU by threading user identity through tool telemetry 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * refactor(web): use try/finally pattern in registerMcpTool for tool_used event 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * fix(web): add error logging in captureEvent catch block Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ae4513e commit 5305019

File tree

14 files changed

+104
-77
lines changed

14 files changed

+104
-77
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
### Changed
1414
- Deprecated `GOOGLE_VERTEX_THINKING_BUDGET_TOKENS` environment variable in favor of per-model `thinkingBudget` config. [#1110](https://github.com/sourcebot-dev/sourcebot/pull/1110)
1515
- Removed `GOOGLE_VERTEX_INCLUDE_THOUGHTS` environment variable. Thoughts are now always included. [#1110](https://github.com/sourcebot-dev/sourcebot/pull/1110)
16+
- 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)
1617

1718
## [4.16.8] - 2026-04-09
1819

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ if (!value) { return; }
5757
if (condition) doSomething();
5858
```
5959

60+
## PostHog Event Naming
61+
62+
- 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`).
63+
- Events fired from multiple sources (web app, MCP server, API) must NOT use the `wa_` prefix (e.g., `ask_message_sent`, `tool_used`).
64+
- Multi-source events should include a `source` property to identify the origin (e.g., `'sourcebot-web-client'`, `'sourcebot-mcp-server'`, `'sourcebot-ask-agent'`).
65+
6066
## Tailwind CSS
6167

6268
Use Tailwind color classes directly instead of CSS variable syntax:

packages/web/src/app/api/(server)/chat/route.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,15 @@ export const POST = apiHandler(async (req: NextRequest) => {
9191
return [];
9292
}))).flat();
9393

94-
await captureEvent('wa_chat_message_sent', {
94+
const source = req.headers.get('X-Sourcebot-Client-Source') ?? undefined;
95+
96+
await captureEvent('ask_message_sent', {
9597
chatId: id,
9698
messageCount: messages.length,
9799
selectedReposCount: expandedRepos.length,
100+
source,
98101
...(env.EXPERIMENT_ASK_GH_ENABLED === 'true' ? { selectedRepos: expandedRepos } : {}),
99-
} );
102+
});
100103

101104
const stream = await createMessageStream({
102105
chatId: id,

packages/web/src/app/api/(server)/search/route.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import { search, searchRequestSchema } from "@/features/search";
44
import { apiHandler } from "@/lib/apiHandler";
5-
import { captureEvent } from "@/lib/posthog";
65
import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
76
import { isServiceError } from "@/lib/utils";
87
import { NextRequest } from "next/server";
@@ -21,12 +20,6 @@ export const POST = apiHandler(async (request: NextRequest) => {
2120
...options
2221
} = parsed.data;
2322

24-
const source = request.headers.get('X-Sourcebot-Client-Source') ?? 'unknown';
25-
await captureEvent('api_code_search_request', {
26-
source,
27-
type: 'blocking',
28-
});
29-
3023
const response = await search({
3124
queryType: 'string',
3225
query,

packages/web/src/app/api/(server)/stream_search/route.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import { streamSearch, searchRequestSchema } from '@/features/search';
44
import { apiHandler } from '@/lib/apiHandler';
5-
import { captureEvent } from '@/lib/posthog';
65
import { requestBodySchemaValidationError, serviceErrorResponse } from '@/lib/serviceError';
76
import { isServiceError } from '@/lib/utils';
87
import { NextRequest } from 'next/server';
@@ -20,12 +19,6 @@ export const POST = apiHandler(async (request: NextRequest) => {
2019
...options
2120
} = parsed.data;
2221

23-
const source = request.headers.get('X-Sourcebot-Client-Source') ?? 'unknown';
24-
await captureEvent('api_code_search_request', {
25-
source,
26-
type: 'streamed',
27-
});
28-
2922
const stream = await streamSearch({
3023
queryType: 'string',
3124
query,

packages/web/src/features/chat/actions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ export const createChat = async ({ source }: { source?: string } = {}) => sew(()
4949
});
5050
}
5151

52-
await captureEvent('wa_chat_thread_created', {
52+
await captureEvent('ask_thread_created', {
5353
chatId: chat.id,
5454
isAnonymous: isGuestUser,
55+
source,
5556
});
5657

5758
return {

packages/web/src/features/chat/agent.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { SBChatMessage, SBChatMessageMetadata } from "@/features/chat/types";
22
import { getAnswerPartFromAssistantMessage } from "@/features/chat/utils";
33
import { getFileSource } from '@/features/git';
4-
import { captureEvent } from "@/lib/posthog";
54
import { isServiceError } from "@/lib/utils";
65
import { LanguageModelV3 as AISDKLanguageModelV3 } from "@ai-sdk/provider";
76
import { ProviderOptions } from "@ai-sdk/provider-utils";
@@ -210,13 +209,7 @@ const createAgentStream = async ({
210209
],
211210
toolChoice: "auto",
212211
onStepFinish: ({ toolResults }) => {
213-
toolResults.forEach(({ toolName, output, dynamic }) => {
214-
captureEvent('wa_chat_tool_used', {
215-
chatId,
216-
toolName,
217-
success: !isServiceError(output),
218-
});
219-
212+
toolResults.forEach(({ output, dynamic }) => {
220213
if (dynamic || isServiceError(output)) {
221214
return;
222215
}

packages/web/src/features/chat/components/chatThread/chatThread.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ export const ChatThread = ({
102102
messages: initialMessages,
103103
transport: new DefaultChatTransport({
104104
api: '/api/chat',
105+
headers: {
106+
'X-Sourcebot-Client-Source': 'sourcebot-web-client',
107+
},
105108
}),
106109
onData: (dataPart) => {
107110
// Keeps sources added by the assistant in sync.

packages/web/src/features/mcp/askCodebase.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,10 @@ export const askCodebase = (params: AskCodebaseParams): Promise<AskCodebaseResul
8686
},
8787
});
8888

89-
await captureEvent('wa_chat_thread_created', {
89+
await captureEvent('ask_thread_created', {
9090
chatId: chat.id,
9191
isAnonymous: !user,
92+
source,
9293
});
9394

9495
if (user) {
@@ -137,10 +138,11 @@ export const askCodebase = (params: AskCodebaseParams): Promise<AskCodebaseResul
137138

138139
let finalMessages: SBChatMessage[] = [];
139140

140-
await captureEvent('wa_chat_message_sent', {
141+
await captureEvent('ask_message_sent', {
141142
chatId: chat.id,
142143
messageCount: 1,
143144
selectedReposCount: selectedRepos.length,
145+
source,
144146
...(env.EXPERIMENT_ASK_GH_ENABLED === 'true' ? {
145147
selectedRepos: selectedRepos.map(r => r.value)
146148
} : {}),

packages/web/src/features/mcp/server.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
languageModelInfoSchema,
33
} from '@/features/chat/types';
44
import { askCodebase } from '@/features/mcp/askCodebase';
5+
import { captureEvent } from '@/lib/posthog';
56
import { isServiceError } from '@/lib/utils';
67
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
78
import { ChatVisibility } from '@sourcebot/db';
@@ -57,6 +58,11 @@ export async function createMcpServer(): Promise<McpServer> {
5758
},
5859
async () => {
5960
const models = await getConfiguredLanguageModelsInfo();
61+
captureEvent('tool_used', {
62+
toolName: 'list_language_models',
63+
source: 'sourcebot-mcp-server',
64+
success: true,
65+
});
6066
return { content: [{ type: "text", text: JSON.stringify(models) }] };
6167
}
6268
);
@@ -101,11 +107,22 @@ export async function createMcpServer(): Promise<McpServer> {
101107
});
102108

103109
if (isServiceError(result)) {
110+
captureEvent('tool_used', {
111+
toolName: 'ask_codebase',
112+
source: 'sourcebot-mcp-server',
113+
success: false,
114+
});
104115
return {
105116
content: [{ type: "text", text: `Failed to ask codebase: ${result.message}` }],
106117
};
107118
}
108119

120+
captureEvent('tool_used', {
121+
toolName: 'ask_codebase',
122+
source: 'sourcebot-mcp-server',
123+
success: true,
124+
});
125+
109126
const formattedResponse = dedent`
110127
${result.answer}
111128

0 commit comments

Comments
 (0)