Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Added PostHog events for chat UI interactions (details card expand/collapse, copy answer, table of contents toggle) and repo tracking in `wa_chat_message_sent`. [#922](https://github.com/sourcebot-dev/sourcebot/pull/922)

## [4.11.7] - 2026-02-23

### Changed
Expand Down
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,18 @@ Branch naming convention:
- Bug fixes: `<username>/fix-<linear_issue_id>`
- If no Linear issue ID is available, omit it from the branch name

PR title should follow conventional commit standards:
- `feat:` new feature or functionality
- `fix:` bug fix
- `docs:` documentation or README changes
- `chore:` maintenance tasks, dependency updates, etc.
- `refactor:` code refactoring without changing behavior
- `test:` adding or updating tests

You can optionally include a scope to indicate which package is affected:
- `feat(web):` feature in the web package
- `fix(worker):` bug fix in the worker package (`backend/`)

PR description:
- If a GitHub issue number was provided, include `Fixes #<github_issue_number>` in the PR description

Expand Down
14 changes: 9 additions & 5 deletions packages/web/src/app/api/(server)/chat/blocking/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
parts: [{ type: 'text', text: query }],
};

const selectedSearchScopes = await Promise.all(repos.map(async (repo) => {
const selectedRepos = (await Promise.all(repos.map(async (repo) => {
const repoDB = await prisma.repo.findFirst({
where: {
name: repo,
Expand All @@ -156,25 +156,29 @@ export const POST = apiHandler(async (request: NextRequest) => {
name: repoDB.displayName ?? repoDB.name.split('/').pop() ?? repoDB.name,
codeHostType: repoDB.external_codeHostType,
} satisfies SearchScope;
}));
})));

// We'll capture the final messages and usage from the stream
let finalMessages: SBChatMessage[] = [];

await captureEvent('wa_chat_message_sent', {
chatId: chat.id,
messageCount: 1,
...(env.EXPERIMENT_ASK_GH_ENABLED === 'true' ? {
selectedRepos: selectedRepos.map(r => r.value)
} : {}),
});

const stream = await createMessageStream({
chatId: chat.id,
messages: [userMessage],
selectedSearchScopes,
metadata: {
selectedSearchScopes: selectedRepos,
},
selectedRepos: selectedRepos.map(r => r.value),
model,
modelName,
modelProviderOptions: providerOptions,
orgId: org.id,
prisma,
onFinish: async ({ messages }) => {
finalMessages = messages;
},
Expand Down
67 changes: 27 additions & 40 deletions packages/web/src/app/api/(server)/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { sew } from "@/actions";
import { _getConfiguredLanguageModelsFull, _getAISDKLanguageModelAndOptions, _updateChatMessages, _isOwnerOfChat } from "@/features/chat/actions";
import { createAgentStream } from "@/features/chat/agent";
import { additionalChatRequestParamsSchema, LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types";
import { additionalChatRequestParamsSchema, LanguageModelInfo, SBChatMessage, SBChatMessageMetadata, SearchScope } from "@/features/chat/types";
import { getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils";
import { apiHandler } from "@/lib/apiHandler";
import { ErrorCode } from "@/lib/errorCodes";
Expand All @@ -10,8 +10,7 @@ import { isServiceError } from "@/lib/utils";
import { withOptionalAuthV2 } from "@/withAuthV2";
import { LanguageModelV2 as AISDKLanguageModelV2 } from "@ai-sdk/provider";
import * as Sentry from "@sentry/nextjs";
import { PrismaClient } from "@sourcebot/db";
import { createLogger } from "@sourcebot/shared";
import { createLogger, env } from "@sourcebot/shared";
import { captureEvent } from "@/lib/posthog";
import {
createUIMessageStream,
Expand Down Expand Up @@ -89,20 +88,34 @@ export const POST = apiHandler(async (req: NextRequest) => {

const { model, providerOptions } = await _getAISDKLanguageModelAndOptions(languageModelConfig);

const expandedRepos = (await Promise.all(selectedSearchScopes.map(async (scope) => {
if (scope.type === 'repo') return [scope.value];
if (scope.type === 'reposet') {
const reposet = await prisma.searchContext.findFirst({
where: { orgId: org.id, name: scope.value },
include: { repos: true }
});
return reposet ? reposet.repos.map(r => r.name) : [];
}
return [];
}))).flat();

await captureEvent('wa_chat_message_sent', {
chatId: id,
messageCount: messages.length,
});
...(env.EXPERIMENT_ASK_GH_ENABLED === 'true' ? { selectedRepos: expandedRepos } : {}),
} );

const stream = await createMessageStream({
chatId: id,
messages,
selectedSearchScopes,
metadata: {
selectedSearchScopes,
},
selectedRepos: expandedRepos,
model,
modelName: languageModelConfig.displayName ?? languageModelConfig.model,
modelProviderOptions: providerOptions,
orgId: org.id,
prisma,
onFinish: async ({ messages }) => {
await _updateChatMessages({ chatId: id, messages, prisma });
},
Expand Down Expand Up @@ -152,25 +165,23 @@ const mergeStreamAsync = async (stream: StreamTextResult<any, any>, writer: UIMe
interface CreateMessageStreamResponseProps {
chatId: string;
messages: SBChatMessage[];
selectedSearchScopes: SearchScope[];
selectedRepos: string[];
model: AISDKLanguageModelV2;
modelName: string;
modelProviderOptions?: Record<string, Record<string, JSONValue>>;
orgId: number;
prisma: PrismaClient;
onFinish: UIMessageStreamOnFinishCallback<SBChatMessage>;
onError: (error: unknown) => string;
modelProviderOptions?: Record<string, Record<string, JSONValue>>;
metadata?: Partial<SBChatMessageMetadata>;
}

export const createMessageStream = async ({
chatId,
messages,
selectedSearchScopes,
metadata,
selectedRepos,
model,
modelName,
modelProviderOptions,
orgId,
prisma,
onFinish,
onError,
}: CreateMessageStreamResponseProps) => {
Expand Down Expand Up @@ -211,36 +222,12 @@ export const createMessageStream = async ({

const startTime = new Date();

const expandedRepos = (await Promise.all(selectedSearchScopes.map(async (scope) => {
if (scope.type === 'repo') {
return [scope.value];
}

if (scope.type === 'reposet') {
const reposet = await prisma.searchContext.findFirst({
where: {
orgId,
name: scope.value
},
include: {
repos: true
}
});

if (reposet) {
return reposet.repos.map(repo => repo.name);
}
}

return [];
}))).flat()

const researchStream = await createAgentStream({
model,
providerOptions: modelProviderOptions,
inputMessages: messageHistory,
inputSources: sources,
selectedRepos: expandedRepos,
selectedRepos,
onWriteSource: (source) => {
writer.write({
type: 'data-source',
Expand All @@ -267,8 +254,8 @@ export const createMessageStream = async ({
totalOutputTokens: totalUsage.outputTokens,
totalResponseTimeMs: new Date().getTime() - startTime.getTime(),
modelName,
selectedSearchScopes,
traceId,
...metadata,
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ const AnswerCardComponent = forwardRef<HTMLDivElement, AnswerCardProps>(({
toast({
description: "✅ Copied to clipboard",
});
captureEvent('wa_chat_copy_answer_pressed', { chatId });
return true;
}, [answerText, toast]);
}, [answerText, chatId, captureEvent, toast]);

const onFeedback = useCallback(async (feedbackType: 'like' | 'dislike') => {
setIsSubmittingFeedback(true);
Expand Down Expand Up @@ -128,7 +129,10 @@ const AnswerCardComponent = forwardRef<HTMLDivElement, AnswerCardProps>(({
<Toggle
className="h-6 w-6 px-3 min-w-6 text-muted-foreground"
pressed={isTOCButtonToggled}
onPressedChange={setIsTOCButtonToggled}
onPressedChange={(next) => {
setIsTOCButtonToggled(next);
captureEvent('wa_chat_toc_toggled', { chatId, isExpanded: next });
}}
>
<TableOfContentsIcon className="h-3 w-3" />
</Toggle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
)}

<DetailsCard
chatId={chatId}
isExpanded={isDetailsPanelExpanded}
onExpandedChanged={onExpandDetailsPanel}
isThinking={isThinking}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { Skeleton } from '@/components/ui/skeleton';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import { Brain, ChevronDown, ChevronRight, Clock, InfoIcon, Loader2, List, ScanSearchIcon, Zap } from 'lucide-react';
import { memo } from 'react';
import { memo, useCallback } from 'react';
import useCaptureEvent from '@/hooks/useCaptureEvent';
import { MarkdownRenderer } from './markdownRenderer';
import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinitionsToolComponent';
import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent';
Expand All @@ -21,6 +22,7 @@ import isEqual from "fast-deep-equal/react";


interface DetailsCardProps {
chatId: string;
isExpanded: boolean;
onExpandedChanged: (isExpanded: boolean) => void;
isThinking: boolean;
Expand All @@ -30,16 +32,24 @@ interface DetailsCardProps {
}

const DetailsCardComponent = ({
chatId,
isExpanded,
onExpandedChanged,
isThinking,
isStreaming,
metadata,
thinkingSteps,
}: DetailsCardProps) => {
const captureEvent = useCaptureEvent();

const handleExpandedChanged = useCallback((next: boolean) => {
captureEvent('wa_chat_details_card_toggled', { chatId, isExpanded: next });
onExpandedChanged(next);
}, [chatId, captureEvent, onExpandedChanged]);

return (
<Card className="mb-4">
<Collapsible open={isExpanded} onOpenChange={onExpandedChanged}>
<Collapsible open={isExpanded} onOpenChange={handleExpandedChanged}>
<CollapsibleTrigger asChild>
<CardContent
className={cn("p-3 cursor-pointer hover:bg-muted", {
Expand Down
17 changes: 17 additions & 0 deletions packages/web/src/lib/posthogEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ export type PosthogEventMap = {
wa_chat_message_sent: {
chatId: string,
messageCount: number,
/**
* @note this field will only be populated when
* the EXPERIMENT_ASK_GH_ENABLED environment variable
* is set to true.
*/
selectedRepos?: string[],
},
wa_chat_tool_used: {
chatId: string,
Expand Down Expand Up @@ -210,6 +216,17 @@ export type PosthogEventMap = {
wa_chat_deleted: {
chatId: string,
},
wa_chat_details_card_toggled: {
chatId: string,
isExpanded: boolean,
},
wa_chat_copy_answer_pressed: {
chatId: string,
},
wa_chat_toc_toggled: {
chatId: string,
isExpanded: boolean,
},
//////////////////////////////////////////////////////////////////
wa_demo_docs_link_pressed: {},
wa_demo_search_example_card_pressed: {
Expand Down