From 9635c0b2d8458c1eff9473251d7d41db4db6c47c Mon Sep 17 00:00:00 2001 From: Max Topolsky Date: Mon, 24 Nov 2025 23:26:22 -0500 Subject: [PATCH 1/5] add copy as markdown to user feedback --- .../feedback/feedbackItem/feedbackActions.tsx | 120 ++++++++++++++++-- 1 file changed, 112 insertions(+), 8 deletions(-) diff --git a/static/app/components/feedback/feedbackItem/feedbackActions.tsx b/static/app/components/feedback/feedbackItem/feedbackActions.tsx index eb4e4a8c897f0e..a238f0a8c2ddd2 100644 --- a/static/app/components/feedback/feedbackItem/feedbackActions.tsx +++ b/static/app/components/feedback/feedbackItem/feedbackActions.tsx @@ -1,5 +1,5 @@ import type {CSSProperties} from 'react'; -import {Fragment} from 'react'; +import {Fragment, useCallback} from 'react'; import {Button} from 'sentry/components/core/button'; import {Flex} from 'sentry/components/core/layout'; @@ -8,11 +8,14 @@ import {DropdownMenu} from 'sentry/components/dropdownMenu'; import ErrorBoundary from 'sentry/components/errorBoundary'; import FeedbackAssignedTo from 'sentry/components/feedback/feedbackItem/feedbackAssignedTo'; import useFeedbackActions from 'sentry/components/feedback/feedbackItem/useFeedbackActions'; -import {IconEllipsis} from 'sentry/icons'; +import {IconCopy, IconEllipsis} from 'sentry/icons'; import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; import type {Group} from 'sentry/types/group'; +import {trackAnalytics} from 'sentry/utils/analytics'; import type {FeedbackIssue} from 'sentry/utils/feedback/types'; +import useCopyToClipboard from 'sentry/utils/useCopyToClipboard'; +import useOrganization from 'sentry/utils/useOrganization'; interface Props { eventData: Event | undefined; @@ -29,6 +32,53 @@ export default function FeedbackActions({ size, style, }: Props) { + const organization = useOrganization(); + const {copy} = useCopyToClipboard(); + const handleCopyToClipboard = useCallback(() => { + const summary = + feedbackItem.metadata.summary ?? + feedbackItem.metadata.title ?? + t('No summary provided'); + const message = + feedbackItem.metadata.message ?? feedbackItem.metadata.value ?? t('No message'); + const culprit = eventData?.culprit?.trim(); + const viewNames = eventData?.contexts?.app?.view_names?.filter(Boolean); + + const sourceLines = []; + if (culprit) { + sourceLines.push(`- ${culprit}`); + } + if (viewNames?.length) { + sourceLines.push(t('- View names: %s', viewNames.join(', '))); + } + + const markdown = [ + '# User Feedback', + '', + `**Summary:** ${summary}`, + '', + '## Feedback Message', + message, + ...(sourceLines.length + ? [ + '', + '## Source (_where user was when feedback was sent_)', + sourceLines.join('\n'), + ] + : []), + ].join('\n'); + + trackAnalytics('feedback.copy-feedback-as-markdown', { + organization, + feedback_id: feedbackItem.id, + project_slug: feedbackItem.project?.slug, + }); + + copy(markdown, { + successMessage: t('Copied feedback summary'), + errorMessage: t('Failed to copy feedback'), + }); + }, [copy, eventData, feedbackItem, organization]); if (!eventData) { return null; } @@ -42,14 +92,35 @@ export default function FeedbackActions({ /> - {size === 'large' ? : null} - {size === 'medium' ? : null} - {size === 'small' ? : null} + {size === 'large' ? ( + + ) : null} + {size === 'medium' ? ( + + ) : null} + {size === 'small' ? ( + + ) : null} ); } -function LargeWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) { +function LargeWidth({ + feedbackItem, + onCopyToClipboard, +}: { + feedbackItem: FeedbackIssue; + onCopyToClipboard: () => void; +}) { const { enableDelete, onDelete, @@ -82,6 +153,15 @@ function LargeWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) { {hasSeen ? t('Mark Unread') : t('Mark Read')} + + - +