Skip to content

Commit 4eec636

Browse files
authored
chore(compass-assistant): add feedback submission and telemetry COMPASS-9608 (#7243)
1 parent 9d17282 commit 4eec636

File tree

6 files changed

+276
-17
lines changed

6 files changed

+276
-17
lines changed

package-lock.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-assistant/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"@mongodb-js/atlas-service": "^0.56.0",
5454
"@mongodb-js/compass-app-registry": "^9.4.20",
5555
"@mongodb-js/compass-components": "^1.49.0",
56+
"@mongodb-js/compass-telemetry": "^1.14.0",
5657
"@mongodb-js/connection-info": "^0.17.1",
5758
"@mongodb-js/compass-logging": "^1.7.12",
5859
"mongodb-connection-string-url": "^3.0.1",

packages/compass-assistant/src/assistant-chat.spec.tsx

Lines changed: 159 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,6 @@ import { createMockChat } from '../test/utils';
1111
import type { AssistantMessage } from './compass-assistant-provider';
1212

1313
describe('AssistantChat', function () {
14-
let originalScrollTo: typeof Element.prototype.scrollTo;
15-
// Mock scrollTo method for DOM elements to prevent test failures
16-
before(function () {
17-
originalScrollTo = Element.prototype.scrollTo.bind(Element.prototype);
18-
Element.prototype.scrollTo = () => {};
19-
});
20-
after(function () {
21-
Element.prototype.scrollTo = originalScrollTo;
22-
});
23-
2414
const mockMessages: AssistantMessage[] = [
2515
{
2616
id: 'user',
@@ -41,8 +31,9 @@ describe('AssistantChat', function () {
4131

4232
function renderWithChat(messages: AssistantMessage[]) {
4333
const chat = createMockChat({ messages });
34+
const result = render(<AssistantChat chat={chat} />);
4435
return {
45-
result: render(<AssistantChat chat={chat} />),
36+
result,
4637
chat,
4738
};
4839
}
@@ -130,8 +121,9 @@ describe('AssistantChat', function () {
130121
);
131122
});
132123

133-
it('calls sendMessage when form is submitted', function () {
134-
const { chat } = renderWithChat([]);
124+
it('calls sendMessage when form is submitted', async function () {
125+
const { chat, result } = renderWithChat([]);
126+
const { track } = result;
135127
const inputField = screen.getByPlaceholderText(
136128
'Ask MongoDB Assistant a question'
137129
);
@@ -142,6 +134,12 @@ describe('AssistantChat', function () {
142134

143135
expect(chat.sendMessage.calledWith({ text: 'What is aggregation?' })).to.be
144136
.true;
137+
138+
await waitFor(() => {
139+
expect(track).to.have.been.calledWith('Assistant Prompt Submitted', {
140+
user_input_length: 'What is aggregation?'.length,
141+
});
142+
});
145143
});
146144

147145
it('clears input field after successful submission', function () {
@@ -159,8 +157,9 @@ describe('AssistantChat', function () {
159157
expect(inputField.value).to.equal('');
160158
});
161159

162-
it('trims whitespace from input before sending', function () {
163-
const { chat } = renderWithChat([]);
160+
it('trims whitespace from input before sending', async function () {
161+
const { chat, result } = renderWithChat([]);
162+
const { track } = result;
164163

165164
const inputField = screen.getByPlaceholderText(
166165
'Ask MongoDB Assistant a question'
@@ -171,6 +170,12 @@ describe('AssistantChat', function () {
171170

172171
expect(chat.sendMessage.calledWith({ text: 'What is sharding?' })).to.be
173172
.true;
173+
174+
await waitFor(() => {
175+
expect(track).to.have.been.calledWith('Assistant Prompt Submitted', {
176+
user_input_length: 'What is sharding?'.length,
177+
});
178+
});
174179
});
175180

176181
it('does not call sendMessage when input is empty or whitespace-only', function () {
@@ -271,4 +276,143 @@ describe('AssistantChat', function () {
271276
expect(screen.queryByText('Another part that should not display.')).to.not
272277
.exist;
273278
});
279+
280+
describe('feedback buttons', function () {
281+
it('shows feedback buttons only for assistant messages', function () {
282+
renderWithChat(mockMessages);
283+
284+
const userMessage = screen.getByTestId('assistant-message-user');
285+
const assistantMessage = screen.getByTestId(
286+
'assistant-message-assistant'
287+
);
288+
289+
// User messages should not have feedback buttons
290+
expect(userMessage.querySelector('[aria-label="Thumbs Up Icon"]')).to.not
291+
.exist;
292+
expect(userMessage.querySelector('[aria-label="Thumbs Down Icon"]')).to
293+
.not.exist;
294+
295+
// Assistant messages should have feedback buttons
296+
expect(assistantMessage.querySelector('[aria-label="Thumbs Up Icon"]')).to
297+
.exist;
298+
expect(assistantMessage.querySelector('[aria-label="Thumbs Down Icon"]'))
299+
.to.exist;
300+
});
301+
302+
it('tracks positive feedback when thumbs up is clicked', async function () {
303+
const { result } = renderWithChat(mockMessages);
304+
const { track } = result;
305+
306+
const assistantMessage = screen.getByTestId(
307+
'assistant-message-assistant'
308+
);
309+
310+
// Find and click the thumbs up button
311+
const thumbsUpButton = assistantMessage.querySelector(
312+
'[aria-label="Thumbs Up Icon"]'
313+
) as HTMLElement;
314+
315+
userEvent.click(thumbsUpButton);
316+
317+
await waitFor(() => {
318+
expect(track).to.have.callCount(1);
319+
expect(track).to.have.been.calledWith('Assistant Feedback Submitted', {
320+
feedback: 'positive',
321+
text: undefined,
322+
request_id: null,
323+
});
324+
});
325+
});
326+
327+
it('tracks negative feedback when thumbs down is clicked', async function () {
328+
const { result } = renderWithChat(mockMessages);
329+
const { track } = result;
330+
331+
const assistantMessage = screen.getByTestId(
332+
'assistant-message-assistant'
333+
);
334+
335+
// Find and click the thumbs down button
336+
const thumbsDownButton = assistantMessage.querySelector(
337+
'[aria-label="Thumbs Down Icon"]'
338+
) as HTMLElement;
339+
340+
userEvent.click(thumbsDownButton);
341+
342+
await waitFor(() => {
343+
expect(track).to.have.callCount(1);
344+
345+
expect(track).to.have.been.calledWith('Assistant Feedback Submitted', {
346+
feedback: 'negative',
347+
text: undefined,
348+
request_id: null,
349+
});
350+
});
351+
});
352+
353+
it('tracks detailed feedback when feedback text is submitted', async function () {
354+
const { result } = renderWithChat(mockMessages);
355+
const { track } = result;
356+
357+
const assistantMessage = screen.getByTestId(
358+
'assistant-message-assistant'
359+
);
360+
361+
// First click thumbs down to potentially open feedback form
362+
const thumbsDownButton = assistantMessage.querySelector(
363+
'[aria-label="Thumbs Down Icon"]'
364+
) as HTMLElement;
365+
366+
userEvent.click(thumbsDownButton);
367+
368+
// Look for feedback text area (the exact implementation depends on LeafyGreen)
369+
const feedbackTextArea = screen.getByTestId(
370+
'lg-chat-message_actions-feedback_textarea'
371+
);
372+
373+
userEvent.type(feedbackTextArea, 'This response was not helpful');
374+
375+
// Look for submit button
376+
const submitButton = screen.getByText('Submit');
377+
378+
userEvent.click(submitButton);
379+
380+
await waitFor(() => {
381+
expect(track).to.have.callCount(2);
382+
383+
expect(track).to.have.been.calledWith('Assistant Feedback Submitted', {
384+
feedback: 'negative',
385+
text: undefined,
386+
request_id: null,
387+
});
388+
389+
expect(track).to.have.been.calledWith('Assistant Feedback Submitted', {
390+
feedback: 'negative',
391+
text: 'This response was not helpful',
392+
request_id: null,
393+
});
394+
});
395+
});
396+
397+
it('does not show feedback buttons when there are no assistant messages', function () {
398+
const userOnlyMessages: AssistantMessage[] = [
399+
{
400+
id: 'user1',
401+
role: 'user',
402+
parts: [{ type: 'text', text: 'Hello!' }],
403+
},
404+
{
405+
id: 'user2',
406+
role: 'user',
407+
parts: [{ type: 'text', text: 'How are you?' }],
408+
},
409+
];
410+
411+
renderWithChat(userOnlyMessages);
412+
413+
// Should not find any feedback buttons in the entire component
414+
expect(screen.queryByLabelText('Thumbs Up Icon')).to.not.exist;
415+
expect(screen.queryByLabelText('Thumbs Down Icon')).to.not.exist;
416+
});
417+
});
274418
});

packages/compass-assistant/src/assistant-chat.tsx

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
LgChatLeafygreenChatProvider,
88
LgChatMessage,
99
LgChatMessageFeed,
10+
LgChatMessageActions,
1011
LgChatInputBar,
1112
spacing,
1213
css,
@@ -16,11 +17,13 @@ import {
1617
palette,
1718
useDarkMode,
1819
} from '@mongodb-js/compass-components';
20+
import { useTelemetry } from '@mongodb-js/compass-telemetry/provider';
1921

2022
const { ChatWindow } = LgChatChatWindow;
2123
const { LeafyGreenChatProvider, Variant } = LgChatLeafygreenChatProvider;
2224
const { Message } = LgChatMessage;
2325
const { MessageFeed } = LgChatMessageFeed;
26+
const { MessageActions } = LgChatMessageActions;
2427
const { InputBar } = LgChatInputBar;
2528

2629
interface AssistantChatProps {
@@ -105,9 +108,15 @@ const errorBannerWrapperStyles = css({
105108
export const AssistantChat: React.FunctionComponent<AssistantChatProps> = ({
106109
chat,
107110
}) => {
111+
const track = useTelemetry();
108112
const darkMode = useDarkMode();
109113
const { messages, sendMessage, status, error, clearError } = useChat({
110114
chat,
115+
onError: (error) => {
116+
track('Assistant Response Failed', () => ({
117+
error_name: error.name,
118+
}));
119+
},
111120
});
112121

113122
// Transform AI SDK messages to LeafyGreen chat format
@@ -127,10 +136,43 @@ export const AssistantChat: React.FunctionComponent<AssistantChatProps> = ({
127136
(messageBody: string) => {
128137
const trimmedMessageBody = messageBody.trim();
129138
if (trimmedMessageBody) {
139+
track('Assistant Prompt Submitted', {
140+
user_input_length: trimmedMessageBody.length,
141+
});
130142
void sendMessage({ text: trimmedMessageBody });
131143
}
132144
},
133-
[sendMessage]
145+
[sendMessage, track]
146+
);
147+
148+
const handleFeedback = useCallback(
149+
(
150+
event,
151+
state:
152+
| {
153+
feedback: string;
154+
rating: string;
155+
}
156+
| {
157+
rating: string;
158+
}
159+
| undefined
160+
) => {
161+
if (!state) {
162+
return;
163+
}
164+
const { rating } = state;
165+
const textFeedback = 'feedback' in state ? state.feedback : undefined;
166+
const feedback: 'positive' | 'negative' =
167+
rating === 'liked' ? 'positive' : 'negative';
168+
169+
track('Assistant Feedback Submitted', {
170+
feedback,
171+
text: textFeedback,
172+
request_id: null,
173+
});
174+
},
175+
[track]
134176
);
135177

136178
return (
@@ -155,7 +197,14 @@ export const AssistantChat: React.FunctionComponent<AssistantChatProps> = ({
155197
sourceType="markdown"
156198
{...messageFields}
157199
data-testid={`assistant-message-${messageFields.id}`}
158-
/>
200+
>
201+
{messageFields.isSender === false && (
202+
<MessageActions
203+
onRatingChange={handleFeedback}
204+
onSubmitFeedback={handleFeedback}
205+
/>
206+
)}
207+
</Message>
159208
))}
160209
{status === 'submitted' && (
161210
<Message

packages/compass-assistant/src/compass-assistant-provider.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { usePreference } from 'compass-preferences-model/provider';
1414
import { createLoggerLocator } from '@mongodb-js/compass-logging/provider';
1515
import type { ConnectionInfo } from '@mongodb-js/connection-info';
1616
import { redactConnectionString } from 'mongodb-connection-string-url';
17+
import { useTelemetry } from '@mongodb-js/compass-telemetry/provider';
1718

1819
export const ASSISTANT_DRAWER_ID = 'compass-assistant-drawer';
1920

@@ -88,6 +89,7 @@ export const AssistantProvider: React.FunctionComponent<
8889
chat: Chat<AssistantMessage>;
8990
}>
9091
> = ({ chat, children }) => {
92+
const track = useTelemetry();
9193
const assistantActionsContext = useRef<AssistantActionsContextType>({
9294
interpretExplainPlan: ({ explainPlan }) => {
9395
openDrawer(ASSISTANT_DRAWER_ID);
@@ -103,6 +105,9 @@ export const AssistantProvider: React.FunctionComponent<
103105
},
104106
{}
105107
);
108+
track('Assistant Entry Point Used', {
109+
source: 'explain plan',
110+
});
106111
},
107112
interpretConnectionError: ({ connectionInfo, error }) => {
108113
openDrawer(ASSISTANT_DRAWER_ID);
@@ -122,6 +127,9 @@ export const AssistantProvider: React.FunctionComponent<
122127
},
123128
{}
124129
);
130+
track('Assistant Entry Point Used', {
131+
source: 'connection error',
132+
});
125133
},
126134
clearChat: () => {
127135
chat.messages = [];

0 commit comments

Comments
 (0)