Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 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 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

Expand Down
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 6 additions & 2 deletions packages/web/src/app/api/(server)/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -109,6 +112,7 @@ 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 });
},
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/app/api/(server)/mcp/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 0 additions & 7 deletions packages/web/src/app/api/(server)/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand Down
7 changes: 0 additions & 7 deletions packages/web/src/app/api/(server)/stream_search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion packages/web/src/features/chat/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
17 changes: 8 additions & 9 deletions packages/web/src/features/chat/agent.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -44,6 +43,8 @@ interface CreateMessageStreamResponseProps {
modelProviderOptions?: Record<string, Record<string, JSONValue>>;
modelTemperature?: number;
metadata?: Partial<SBChatMessageMetadata>;
/** User ID for telemetry attribution on tool_used events. */
userId?: string;
}

export const createMessageStream = async ({
Expand All @@ -57,6 +58,7 @@ export const createMessageStream = async ({
modelTemperature,
onFinish,
onError,
userId,
}: CreateMessageStreamResponseProps) => {
const latestMessage = messages[messages.length - 1];
const sources = latestMessage.parts
Expand Down Expand Up @@ -102,6 +104,7 @@ export const createMessageStream = async ({
inputMessages: messageHistory,
inputSources: sources,
selectedRepos,
userId,
onWriteSource: (source) => {
writer.write({
type: 'data-source',
Expand Down Expand Up @@ -155,6 +158,7 @@ interface AgentOptions {
onWriteSource: (source: Source) => void;
traceId: string;
chatId: string;
userId?: string;
}

const createAgentStream = async ({
Expand All @@ -167,6 +171,7 @@ 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');
Expand Down Expand Up @@ -203,20 +208,14 @@ const createAgentStream = async ({
providerOptions,
messages: inputMessages,
system: systemPrompt,
tools: createTools({ source: 'sourcebot-ask-agent', selectedRepos }),
tools: createTools({ source: 'sourcebot-ask-agent', selectedRepos, userId }),
temperature: temperature ?? env.SOURCEBOT_CHAT_MODEL_TEMPERATURE,
stopWhen: [
stepCountIsGTE(env.SOURCEBOT_CHAT_MAX_STEP_COUNT),
],
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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions packages/web/src/features/mcp/askCodebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ export const askCodebase = (params: AskCodebaseParams): Promise<AskCodebaseResul
},
});

await captureEvent('wa_chat_thread_created', {
await captureEvent('ask_thread_created', {
chatId: chat.id,
isAnonymous: !user,
source,
});

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

let finalMessages: SBChatMessage[] = [];

await captureEvent('wa_chat_message_sent', {
await captureEvent('ask_message_sent', {
chatId: chat.id,
messageCount: 1,
selectedReposCount: selectedRepos.length,
source,
...(env.EXPERIMENT_ASK_GH_ENABLED === 'true' ? {
selectedRepos: selectedRepos.map(r => r.value)
} : {}),
Expand All @@ -157,6 +159,7 @@ export const askCodebase = (params: AskCodebaseParams): Promise<AskCodebaseResul
modelName,
modelProviderOptions: providerOptions,
modelTemperature: temperature,
userId: user?.id,
onFinish: async ({ messages }) => {
finalMessages = messages;
},
Expand Down
20 changes: 19 additions & 1 deletion packages/web/src/features/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,7 +25,7 @@ import {

const dedent = _dedent.withOptions({ alignValues: true });

export async function createMcpServer(): Promise<McpServer> {
export async function createMcpServer(options?: { userId?: string }): Promise<McpServer> {
const server = new McpServer({
name: 'sourcebot-mcp-server',
version: SOURCEBOT_VERSION,
Expand All @@ -35,6 +36,7 @@ export async function createMcpServer(): Promise<McpServer> {

const toolContext: ToolContext = {
source: 'sourcebot-mcp-server',
userId: options?.userId,
}

registerMcpTool(server, grepDefinition, toolContext);
Expand All @@ -57,6 +59,11 @@ export async function createMcpServer(): Promise<McpServer> {
},
async () => {
const models = await getConfiguredLanguageModelsInfo();
captureEvent('tool_used', {
toolName: 'list_language_models',
source: 'sourcebot-mcp-server',
success: true,
}, { distinctId: options?.userId });
return { content: [{ type: "text", text: JSON.stringify(models) }] };
}
);
Expand Down Expand Up @@ -101,11 +108,22 @@ export async function createMcpServer(): Promise<McpServer> {
});

if (isServiceError(result)) {
captureEvent('tool_used', {
toolName: 'ask_codebase',
source: 'sourcebot-mcp-server',
success: false,
}, { distinctId: options?.userId });
return {
content: [{ type: "text", text: `Failed to ask codebase: ${result.message}` }],
};
}

captureEvent('tool_used', {
toolName: 'ask_codebase',
source: 'sourcebot-mcp-server',
success: true,
}, { distinctId: options?.userId });

const formattedResponse = dedent`
${result.answer}

Expand Down
25 changes: 24 additions & 1 deletion packages/web/src/features/tools/adapters.ts
Original file line number Diff line number Diff line change
@@ -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<TName extends string, TShape extends z.ZodRawShape, TMetadata>(
Expand All @@ -11,7 +12,21 @@ export function toVercelAITool<TName extends string, TShape extends z.ZodRawShap
description: def.description,
inputSchema: def.inputSchema,
title: def.title,
execute: (input) => 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,
}, { distinctId: context.userId });
}
},
toModelOutput: ({ output }) => ({
type: "content",
value: [{ type: "text", text: output.output }],
Expand All @@ -38,13 +53,21 @@ export function registerMcpTool<TName extends string, TShape extends z.ZodRawSha
},
},
async (input) => {
let success = true;
try {
const parsed = def.inputSchema.parse(input);
const result = await def.execute(parsed, context);
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,
}, { distinctId: context.userId });
}
},
);
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/features/tools/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export type Source = z.infer<typeof sourceSchema>;
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<
Expand Down
4 changes: 1 addition & 3 deletions packages/web/src/lib/apiHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ export function apiHandler<H extends AnyHandler>(
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
Expand Down
Loading
Loading