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()}>
+ }
+ onClick={e => handleFeedback(true, e)}
+ aria-label={t('This was helpful')}
+ />
+ }
+ onClick={e => handleFeedback(false, e)}
+ aria-label={t('This was not helpful')}
+ />
+
+ );
+}
+
export function GroupSummaryWithAutofix({
group,
event,
@@ -157,6 +214,7 @@ export function AutofixSummary({
const organization = useOrganization();
const navigate = useNavigate();
const location = useLocation();
+ const {data: autofixData} = useAutofixData({groupId: group.id});
const seerLink = {
pathname: location.pathname,
@@ -172,6 +230,7 @@ export function AutofixSummary({
title: t('Root Cause'),
insight: rootCauseDescription,
icon: ,
+ feedbackType: 'root_cause',
onClick: () => {
trackAnalytics('autofix.summary_root_cause_clicked', {
organization,
@@ -199,6 +258,7 @@ export function AutofixSummary({
insight: solutionDescription,
icon: ,
isLoading: solutionIsLoading,
+ feedbackType: 'solution' as const,
onClick: () => {
trackAnalytics('autofix.summary_solution_clicked', {
organization,
@@ -228,6 +288,7 @@ export function AutofixSummary({
insight: codeChangesDescription,
icon: ,
isLoading: codeChangesIsLoading,
+ feedbackType: 'changes' as const,
onClick: () => {
trackAnalytics('autofix.summary_code_changes_clicked', {
organization,
@@ -263,20 +324,29 @@ export function AutofixSummary({
{card.icon}
{card.title}
- {card.copyText && card.copyTitle && (
- {
- e.stopPropagation();
- }}
- analyticsEventName={card.copyAnalyticsEventName}
- analyticsEventKey={card.copyAnalyticsEventKey}
- />
- )}
+
+ {!card.isLoading && card.feedbackType && autofixData?.run_id && (
+
+ )}
+ {card.copyText && card.copyTitle && (
+ {
+ e.stopPropagation();
+ }}
+ analyticsEventName={card.copyAnalyticsEventName}
+ analyticsEventKey={card.copyAnalyticsEventKey}
+ />
+ )}
+
{card.isLoading ? (
@@ -425,3 +495,21 @@ const CardContent = styled('div')`
}
}
`;
+
+const CardActions = styled('div')`
+ display: flex;
+ align-items: center;
+ gap: ${space(0.5)};
+`;
+
+const FeedbackContainer = styled('div')`
+ display: flex;
+ align-items: center;
+ gap: ${space(0.25)};
+`;
+
+const FeedbackText = styled('span')`
+ font-size: ${p => p.theme.fontSize.xs};
+ color: ${p => p.theme.subText};
+ padding: 0 ${space(0.5)};
+`;