diff --git a/static/app/components/group/groupSummaryWithAutofix.spec.tsx b/static/app/components/group/groupSummaryWithAutofix.spec.tsx new file mode 100644 index 00000000000000..60f0da9b569f4f --- /dev/null +++ b/static/app/components/group/groupSummaryWithAutofix.spec.tsx @@ -0,0 +1,278 @@ +import {GroupFixture} from 'sentry-fixture/group'; +import {OrganizationFixture} from 'sentry-fixture/organization'; +import {ProjectFixture} from 'sentry-fixture/project'; +import {UserFixture} from 'sentry-fixture/user'; + +import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; + +import {useAutofixData} from 'sentry/components/events/autofix/useAutofix'; +import {AutofixSummary} from 'sentry/components/group/groupSummaryWithAutofix'; +import {trackAnalytics} from 'sentry/utils/analytics'; + +jest.mock('sentry/components/events/autofix/useAutofix'); +jest.mock('sentry/utils/analytics'); + +describe('AutofixSummary', () => { + const organization = OrganizationFixture(); + const project = ProjectFixture(); + const group = GroupFixture({id: '1', shortId: 'TEST-1'}); + const user = UserFixture(); + + beforeEach(() => { + jest.clearAllMocks(); + MockApiClient.clearMockResponses(); + + jest.mocked(useAutofixData).mockReturnValue({ + data: { + run_id: 'test-run-id', + status: 'COMPLETED', + steps: [], + }, + isPending: false, + isError: false, + error: null, + } as any); + }); + + it('renders feedback buttons for root cause card', async () => { + render( + , + {organization} + ); + + expect(screen.getByRole('button', {name: 'This was helpful'})).toBeInTheDocument(); + expect( + screen.getByRole('button', {name: 'This was not helpful'}) + ).toBeInTheDocument(); + }); + + it('renders feedback buttons for solution card', async () => { + render( + , + {organization} + ); + + const helpfulButtons = screen.getAllByRole('button', {name: 'This was helpful'}); + expect(helpfulButtons).toHaveLength(2); // One for root cause, one for solution + }); + + it('tracks analytics event when thumbs up is clicked', async () => { + render( + , + {organization, user} + ); + + const thumbsUpButton = screen.getByRole('button', {name: 'This was helpful'}); + await userEvent.click(thumbsUpButton); + + await waitFor(() => { + expect(trackAnalytics).toHaveBeenCalledWith( + 'seer.autofix.feedback_submitted', + expect.objectContaining({ + step_type: 'root_cause', + positive: true, + group_id: '1', + autofix_run_id: 'test-run-id', + user_id: user.id, + organization, + }) + ); + }); + }); + + it('tracks analytics event when thumbs down is clicked', async () => { + render( + , + {organization, user} + ); + + const thumbsDownButton = screen.getByRole('button', { + name: 'This was not helpful', + }); + await userEvent.click(thumbsDownButton); + + await waitFor(() => { + expect(trackAnalytics).toHaveBeenCalledWith( + 'seer.autofix.feedback_submitted', + expect.objectContaining({ + step_type: 'root_cause', + positive: false, + group_id: '1', + autofix_run_id: 'test-run-id', + user_id: user.id, + organization, + }) + ); + }); + }); + + it('shows "Thanks!" message after feedback is submitted', async () => { + render( + , + {organization} + ); + + const thumbsUpButton = screen.getByRole('button', {name: 'This was helpful'}); + await userEvent.click(thumbsUpButton); + + expect(await screen.findByText('Thanks!')).toBeInTheDocument(); + expect( + screen.queryByRole('button', {name: 'This was helpful'}) + ).not.toBeInTheDocument(); + }); + + it('does not render feedback buttons when loading', async () => { + render( + , + {organization} + ); + + // Root cause should have feedback buttons + expect(screen.getByRole('button', {name: 'This was helpful'})).toBeInTheDocument(); + + // Solution should not have feedback buttons because it's loading + const helpfulButtons = screen.getAllByRole('button', {name: 'This was helpful'}); + expect(helpfulButtons).toHaveLength(1); + }); + + it('tracks different step_type for solution feedback', async () => { + render( + , + {organization, user} + ); + + const thumbsUpButtons = screen.getAllByRole('button', {name: 'This was helpful'}); + // Click the second thumbs up (solution) + await userEvent.click(thumbsUpButtons[1]); + + await waitFor(() => { + expect(trackAnalytics).toHaveBeenCalledWith( + 'seer.autofix.feedback_submitted', + expect.objectContaining({ + step_type: 'solution', + positive: true, + }) + ); + }); + }); + + it('tracks changes step_type for code changes feedback', async () => { + render( + , + {organization, user} + ); + + const thumbsUpButtons = screen.getAllByRole('button', {name: 'This was helpful'}); + // Click the third thumbs up (code changes) + await userEvent.click(thumbsUpButtons[2]); + + await waitFor(() => { + expect(trackAnalytics).toHaveBeenCalledWith( + 'seer.autofix.feedback_submitted', + expect.objectContaining({ + step_type: 'changes', + positive: true, + }) + ); + }); + }); + + it('does not render feedback buttons when run_id is missing', async () => { + jest.mocked(useAutofixData).mockReturnValue({ + data: null, + isPending: false, + isError: false, + error: null, + } as any); + + render( + , + {organization} + ); + + expect( + screen.queryByRole('button', {name: 'This was helpful'}) + ).not.toBeInTheDocument(); + }); +}); diff --git a/static/app/components/group/groupSummaryWithAutofix.tsx b/static/app/components/group/groupSummaryWithAutofix.tsx index 5b53fe65687054..6522cbe6145548 100644 --- a/static/app/components/group/groupSummaryWithAutofix.tsx +++ b/static/app/components/group/groupSummaryWithAutofix.tsx @@ -1,8 +1,9 @@ -import React, {useMemo} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import {motion} from 'framer-motion'; import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton'; +import {Button} from 'sentry/components/core/button'; import {useAutofixData} from 'sentry/components/events/autofix/useAutofix'; import { getAutofixRunExists, @@ -17,7 +18,7 @@ import { } from 'sentry/components/events/autofix/utils'; import {GroupSummary} from 'sentry/components/group/groupSummary'; import Placeholder from 'sentry/components/placeholder'; -import {IconCode, IconFix, IconFocus} from 'sentry/icons'; +import {IconCode, IconFix, IconFocus, IconThumb} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Event} from 'sentry/types/event'; @@ -30,6 +31,7 @@ import testableTransition from 'sentry/utils/testableTransition'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; +import {useUser} from 'sentry/utils/useUser'; const pulseAnimation = { initial: {opacity: 1}, @@ -51,12 +53,67 @@ interface InsightCardObject { copyAnalyticsEventName?: string; copyText?: string | null; copyTitle?: string | null; + feedbackType?: 'root_cause' | 'solution' | 'changes'; icon?: React.ReactNode; insightElement?: React.ReactNode; isLoading?: boolean; onClick?: () => void; } +interface CardFeedbackProps { + feedbackType: 'root_cause' | 'solution' | 'changes'; + groupId: string; + runId: string; +} + +function CardFeedback({feedbackType, groupId, runId}: CardFeedbackProps) { + const [feedbackSubmitted, setFeedbackSubmitted] = useState(false); + const organization = useOrganization(); + const user = useUser(); + + const handleFeedback = useCallback( + (positive: boolean, e: React.MouseEvent) => { + e.stopPropagation(); + + const analyticsData = { + step_type: feedbackType, + positive, + group_id: groupId, + autofix_run_id: runId, + user_id: user.id, + organization, + }; + + trackAnalytics('seer.autofix.feedback_submitted', analyticsData); + setFeedbackSubmitted(true); + }, + [feedbackType, groupId, runId, organization, user] + ); + + if (feedbackSubmitted) { + return e.stopPropagation()}>{t('Thanks!')}; + } + + return ( + e.stopPropagation()}> +