Skip to content

Commit 5be4667

Browse files
chore: Add additional PostHog events for chat repo tracking and UI interactions (#922)
* Add PostHog events for chat repo tracking and UI interactions - Track selected repos in `wa_chat_message_sent` (SaaS-only, gated on EXPERIMENT_ASK_GH_ENABLED) - Refactor `createMessageStream` to accept pre-expanded repos instead of expanding internally - Add `wa_chat_details_card_toggled`, `wa_chat_copy_answer_pressed`, `wa_chat_toc_toggled` events - Add @saasOnly / @experimentonly JSDoc convention for SaaS-only properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * changelog Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * update claude.md * nits --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e58aff1 commit 5be4667

File tree

8 files changed

+87
-49
lines changed

8 files changed

+87
-49
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- 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)
12+
1013
## [4.11.7] - 2026-02-23
1114

1215
### Changed

CLAUDE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,18 @@ Branch naming convention:
193193
- Bug fixes: `<username>/fix-<linear_issue_id>`
194194
- If no Linear issue ID is available, omit it from the branch name
195195

196+
PR title should follow conventional commit standards:
197+
- `feat:` new feature or functionality
198+
- `fix:` bug fix
199+
- `docs:` documentation or README changes
200+
- `chore:` maintenance tasks, dependency updates, etc.
201+
- `refactor:` code refactoring without changing behavior
202+
- `test:` adding or updating tests
203+
204+
You can optionally include a scope to indicate which package is affected:
205+
- `feat(web):` feature in the web package
206+
- `fix(worker):` bug fix in the worker package (`backend/`)
207+
196208
PR description:
197209
- If a GitHub issue number was provided, include `Fixes #<github_issue_number>` in the PR description
198210

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
135135
parts: [{ type: 'text', text: query }],
136136
};
137137

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

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

164164
await captureEvent('wa_chat_message_sent', {
165165
chatId: chat.id,
166166
messageCount: 1,
167+
...(env.EXPERIMENT_ASK_GH_ENABLED === 'true' ? {
168+
selectedRepos: selectedRepos.map(r => r.value)
169+
} : {}),
167170
});
168171

169172
const stream = await createMessageStream({
170173
chatId: chat.id,
171174
messages: [userMessage],
172-
selectedSearchScopes,
175+
metadata: {
176+
selectedSearchScopes: selectedRepos,
177+
},
178+
selectedRepos: selectedRepos.map(r => r.value),
173179
model,
174180
modelName,
175181
modelProviderOptions: providerOptions,
176-
orgId: org.id,
177-
prisma,
178182
onFinish: async ({ messages }) => {
179183
finalMessages = messages;
180184
},

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

Lines changed: 27 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { sew } from "@/actions";
22
import { _getConfiguredLanguageModelsFull, _getAISDKLanguageModelAndOptions, _updateChatMessages, _isOwnerOfChat } from "@/features/chat/actions";
33
import { createAgentStream } from "@/features/chat/agent";
4-
import { additionalChatRequestParamsSchema, LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types";
4+
import { additionalChatRequestParamsSchema, LanguageModelInfo, SBChatMessage, SBChatMessageMetadata, SearchScope } from "@/features/chat/types";
55
import { getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils";
66
import { apiHandler } from "@/lib/apiHandler";
77
import { ErrorCode } from "@/lib/errorCodes";
@@ -10,8 +10,7 @@ import { isServiceError } from "@/lib/utils";
1010
import { withOptionalAuthV2 } from "@/withAuthV2";
1111
import { LanguageModelV2 as AISDKLanguageModelV2 } from "@ai-sdk/provider";
1212
import * as Sentry from "@sentry/nextjs";
13-
import { PrismaClient } from "@sourcebot/db";
14-
import { createLogger } from "@sourcebot/shared";
13+
import { createLogger, env } from "@sourcebot/shared";
1514
import { captureEvent } from "@/lib/posthog";
1615
import {
1716
createUIMessageStream,
@@ -89,20 +88,34 @@ export const POST = apiHandler(async (req: NextRequest) => {
8988

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

91+
const expandedRepos = (await Promise.all(selectedSearchScopes.map(async (scope) => {
92+
if (scope.type === 'repo') return [scope.value];
93+
if (scope.type === 'reposet') {
94+
const reposet = await prisma.searchContext.findFirst({
95+
where: { orgId: org.id, name: scope.value },
96+
include: { repos: true }
97+
});
98+
return reposet ? reposet.repos.map(r => r.name) : [];
99+
}
100+
return [];
101+
}))).flat();
102+
92103
await captureEvent('wa_chat_message_sent', {
93104
chatId: id,
94105
messageCount: messages.length,
95-
});
106+
...(env.EXPERIMENT_ASK_GH_ENABLED === 'true' ? { selectedRepos: expandedRepos } : {}),
107+
} );
96108

97109
const stream = await createMessageStream({
98110
chatId: id,
99111
messages,
100-
selectedSearchScopes,
112+
metadata: {
113+
selectedSearchScopes,
114+
},
115+
selectedRepos: expandedRepos,
101116
model,
102117
modelName: languageModelConfig.displayName ?? languageModelConfig.model,
103118
modelProviderOptions: providerOptions,
104-
orgId: org.id,
105-
prisma,
106119
onFinish: async ({ messages }) => {
107120
await _updateChatMessages({ chatId: id, messages, prisma });
108121
},
@@ -152,25 +165,23 @@ const mergeStreamAsync = async (stream: StreamTextResult<any, any>, writer: UIMe
152165
interface CreateMessageStreamResponseProps {
153166
chatId: string;
154167
messages: SBChatMessage[];
155-
selectedSearchScopes: SearchScope[];
168+
selectedRepos: string[];
156169
model: AISDKLanguageModelV2;
157170
modelName: string;
158-
modelProviderOptions?: Record<string, Record<string, JSONValue>>;
159-
orgId: number;
160-
prisma: PrismaClient;
161171
onFinish: UIMessageStreamOnFinishCallback<SBChatMessage>;
162172
onError: (error: unknown) => string;
173+
modelProviderOptions?: Record<string, Record<string, JSONValue>>;
174+
metadata?: Partial<SBChatMessageMetadata>;
163175
}
164176

165177
export const createMessageStream = async ({
166178
chatId,
167179
messages,
168-
selectedSearchScopes,
180+
metadata,
181+
selectedRepos,
169182
model,
170183
modelName,
171184
modelProviderOptions,
172-
orgId,
173-
prisma,
174185
onFinish,
175186
onError,
176187
}: CreateMessageStreamResponseProps) => {
@@ -211,36 +222,12 @@ export const createMessageStream = async ({
211222

212223
const startTime = new Date();
213224

214-
const expandedRepos = (await Promise.all(selectedSearchScopes.map(async (scope) => {
215-
if (scope.type === 'repo') {
216-
return [scope.value];
217-
}
218-
219-
if (scope.type === 'reposet') {
220-
const reposet = await prisma.searchContext.findFirst({
221-
where: {
222-
orgId,
223-
name: scope.value
224-
},
225-
include: {
226-
repos: true
227-
}
228-
});
229-
230-
if (reposet) {
231-
return reposet.repos.map(repo => repo.name);
232-
}
233-
}
234-
235-
return [];
236-
}))).flat()
237-
238225
const researchStream = await createAgentStream({
239226
model,
240227
providerOptions: modelProviderOptions,
241228
inputMessages: messageHistory,
242229
inputSources: sources,
243-
selectedRepos: expandedRepos,
230+
selectedRepos,
244231
onWriteSource: (source) => {
245232
writer.write({
246233
type: 'data-source',
@@ -267,8 +254,8 @@ export const createMessageStream = async ({
267254
totalOutputTokens: totalUsage.outputTokens,
268255
totalResponseTimeMs: new Date().getTime() - startTime.getTime(),
269256
modelName,
270-
selectedSearchScopes,
271257
traceId,
258+
...metadata,
272259
}
273260
});
274261

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ const AnswerCardComponent = forwardRef<HTMLDivElement, AnswerCardProps>(({
5757
toast({
5858
description: "✅ Copied to clipboard",
5959
});
60+
captureEvent('wa_chat_copy_answer_pressed', { chatId });
6061
return true;
61-
}, [answerText, toast]);
62+
}, [answerText, chatId, captureEvent, toast]);
6263

6364
const onFeedback = useCallback(async (feedbackType: 'like' | 'dislike') => {
6465
setIsSubmittingFeedback(true);
@@ -128,7 +129,10 @@ const AnswerCardComponent = forwardRef<HTMLDivElement, AnswerCardProps>(({
128129
<Toggle
129130
className="h-6 w-6 px-3 min-w-6 text-muted-foreground"
130131
pressed={isTOCButtonToggled}
131-
onPressedChange={setIsTOCButtonToggled}
132+
onPressedChange={(next) => {
133+
setIsTOCButtonToggled(next);
134+
captureEvent('wa_chat_toc_toggled', { chatId, isExpanded: next });
135+
}}
132136
>
133137
<TableOfContentsIcon className="h-3 w-3" />
134138
</Toggle>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
349349
)}
350350

351351
<DetailsCard
352+
chatId={chatId}
352353
isExpanded={isDetailsPanelExpanded}
353354
onExpandedChanged={onExpandDetailsPanel}
354355
isThinking={isThinking}

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { Skeleton } from '@/components/ui/skeleton';
77
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
88
import { cn } from '@/lib/utils';
99
import { Brain, ChevronDown, ChevronRight, Clock, InfoIcon, Loader2, List, ScanSearchIcon, Zap } from 'lucide-react';
10-
import { memo } from 'react';
10+
import { memo, useCallback } from 'react';
11+
import useCaptureEvent from '@/hooks/useCaptureEvent';
1112
import { MarkdownRenderer } from './markdownRenderer';
1213
import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinitionsToolComponent';
1314
import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent';
@@ -21,6 +22,7 @@ import isEqual from "fast-deep-equal/react";
2122

2223

2324
interface DetailsCardProps {
25+
chatId: string;
2426
isExpanded: boolean;
2527
onExpandedChanged: (isExpanded: boolean) => void;
2628
isThinking: boolean;
@@ -30,16 +32,24 @@ interface DetailsCardProps {
3032
}
3133

3234
const DetailsCardComponent = ({
35+
chatId,
3336
isExpanded,
3437
onExpandedChanged,
3538
isThinking,
3639
isStreaming,
3740
metadata,
3841
thinkingSteps,
3942
}: DetailsCardProps) => {
43+
const captureEvent = useCaptureEvent();
44+
45+
const handleExpandedChanged = useCallback((next: boolean) => {
46+
captureEvent('wa_chat_details_card_toggled', { chatId, isExpanded: next });
47+
onExpandedChanged(next);
48+
}, [chatId, captureEvent, onExpandedChanged]);
49+
4050
return (
4151
<Card className="mb-4">
42-
<Collapsible open={isExpanded} onOpenChange={onExpandedChanged}>
52+
<Collapsible open={isExpanded} onOpenChange={handleExpandedChanged}>
4353
<CollapsibleTrigger asChild>
4454
<CardContent
4555
className={cn("p-3 cursor-pointer hover:bg-muted", {

packages/web/src/lib/posthogEvents.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ export type PosthogEventMap = {
157157
wa_chat_message_sent: {
158158
chatId: string,
159159
messageCount: number,
160+
/**
161+
* @note this field will only be populated when
162+
* the EXPERIMENT_ASK_GH_ENABLED environment variable
163+
* is set to true.
164+
*/
165+
selectedRepos?: string[],
160166
},
161167
wa_chat_tool_used: {
162168
chatId: string,
@@ -210,6 +216,17 @@ export type PosthogEventMap = {
210216
wa_chat_deleted: {
211217
chatId: string,
212218
},
219+
wa_chat_details_card_toggled: {
220+
chatId: string,
221+
isExpanded: boolean,
222+
},
223+
wa_chat_copy_answer_pressed: {
224+
chatId: string,
225+
},
226+
wa_chat_toc_toggled: {
227+
chatId: string,
228+
isExpanded: boolean,
229+
},
213230
//////////////////////////////////////////////////////////////////
214231
wa_demo_docs_link_pressed: {},
215232
wa_demo_search_example_card_pressed: {

0 commit comments

Comments
 (0)