Skip to content

Commit e195c19

Browse files
authored
add copy as markdown to user feedback (#103954)
<img width="1800" height="130" alt="image" src="https://github.com/user-attachments/assets/495ea37a-8333-48d3-8588-061557aa248e" /> copy feedback as markdown to make it easier to dump into LLM. For reviewers, talked this over with @jas-kas and see it as a low-hanging fruit addition that could be helpful for addressing individual feedback items
1 parent 9120ec2 commit e195c19

File tree

2 files changed

+108
-8
lines changed

2 files changed

+108
-8
lines changed

static/app/components/feedback/feedbackItem/feedbackActions.tsx

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {CSSProperties} from 'react';
2-
import {Fragment} from 'react';
2+
import {Fragment, useCallback} from 'react';
33

44
import {Button} from 'sentry/components/core/button';
55
import {Flex} from 'sentry/components/core/layout';
@@ -8,11 +8,14 @@ import {DropdownMenu} from 'sentry/components/dropdownMenu';
88
import ErrorBoundary from 'sentry/components/errorBoundary';
99
import FeedbackAssignedTo from 'sentry/components/feedback/feedbackItem/feedbackAssignedTo';
1010
import useFeedbackActions from 'sentry/components/feedback/feedbackItem/useFeedbackActions';
11-
import {IconEllipsis} from 'sentry/icons';
11+
import {IconCopy, IconEllipsis} from 'sentry/icons';
1212
import {t} from 'sentry/locale';
1313
import type {Event} from 'sentry/types/event';
1414
import type {Group} from 'sentry/types/group';
15+
import {trackAnalytics} from 'sentry/utils/analytics';
1516
import type {FeedbackIssue} from 'sentry/utils/feedback/types';
17+
import useCopyToClipboard from 'sentry/utils/useCopyToClipboard';
18+
import useOrganization from 'sentry/utils/useOrganization';
1619

1720
interface Props {
1821
eventData: Event | undefined;
@@ -29,6 +32,47 @@ export default function FeedbackActions({
2932
size,
3033
style,
3134
}: Props) {
35+
const organization = useOrganization();
36+
const {copy} = useCopyToClipboard();
37+
const handleCopyToClipboard = useCallback(() => {
38+
const summary = feedbackItem.metadata.summary;
39+
const message =
40+
feedbackItem.metadata.message ?? feedbackItem.metadata.value ?? t('No message');
41+
const culprit = eventData?.culprit?.trim();
42+
const viewNames = eventData?.contexts?.app?.view_names?.filter(Boolean);
43+
44+
const sourceLines = [];
45+
if (culprit) {
46+
sourceLines.push(`- ${culprit}`);
47+
}
48+
if (viewNames?.length) {
49+
sourceLines.push(t('- View names: %s', viewNames.join(', ')));
50+
}
51+
52+
const markdown = [
53+
'# User Feedback',
54+
'',
55+
...(summary ? [`**Summary:** ${summary}`, ''] : []),
56+
'## Feedback Message',
57+
message,
58+
...(sourceLines.length
59+
? [
60+
'',
61+
'## Source (_where user was when feedback was sent_)',
62+
sourceLines.join('\n'),
63+
]
64+
: []),
65+
].join('\n');
66+
67+
trackAnalytics('feedback.feedback-item-copy-as-markdown', {
68+
organization,
69+
});
70+
71+
copy(markdown, {
72+
successMessage: t('Copied feedback'),
73+
errorMessage: t('Failed to copy feedback'),
74+
});
75+
}, [copy, eventData, feedbackItem, organization]);
3276
if (!eventData) {
3377
return null;
3478
}
@@ -42,14 +86,35 @@ export default function FeedbackActions({
4286
/>
4387
</ErrorBoundary>
4488

45-
{size === 'large' ? <LargeWidth feedbackItem={feedbackItem} /> : null}
46-
{size === 'medium' ? <MediumWidth feedbackItem={feedbackItem} /> : null}
47-
{size === 'small' ? <SmallWidth feedbackItem={feedbackItem} /> : null}
89+
{size === 'large' ? (
90+
<LargeWidth
91+
feedbackItem={feedbackItem}
92+
onCopyToClipboard={handleCopyToClipboard}
93+
/>
94+
) : null}
95+
{size === 'medium' ? (
96+
<MediumWidth
97+
feedbackItem={feedbackItem}
98+
onCopyToClipboard={handleCopyToClipboard}
99+
/>
100+
) : null}
101+
{size === 'small' ? (
102+
<SmallWidth
103+
feedbackItem={feedbackItem}
104+
onCopyToClipboard={handleCopyToClipboard}
105+
/>
106+
) : null}
48107
</Flex>
49108
);
50109
}
51110

52-
function LargeWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
111+
function LargeWidth({
112+
feedbackItem,
113+
onCopyToClipboard,
114+
}: {
115+
feedbackItem: FeedbackIssue;
116+
onCopyToClipboard: () => void;
117+
}) {
53118
const {
54119
enableDelete,
55120
onDelete,
@@ -82,6 +147,15 @@ function LargeWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
82147
{hasSeen ? t('Mark Unread') : t('Mark Read')}
83148
</Button>
84149
</Tooltip>
150+
<Tooltip title={t('Copy feedback as markdown')}>
151+
<Button
152+
size="xs"
153+
priority="default"
154+
icon={<IconCopy />}
155+
onClick={onCopyToClipboard}
156+
aria-label={t('Copy feedback as markdown')}
157+
/>
158+
</Tooltip>
85159
<Tooltip
86160
disabled={enableDelete}
87161
title={t('You must be an admin to delete feedback')}
@@ -94,7 +168,13 @@ function LargeWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
94168
);
95169
}
96170

97-
function MediumWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
171+
function MediumWidth({
172+
feedbackItem,
173+
onCopyToClipboard,
174+
}: {
175+
feedbackItem: FeedbackIssue;
176+
onCopyToClipboard: () => void;
177+
}) {
98178
const {
99179
enableDelete,
100180
onDelete,
@@ -140,6 +220,12 @@ function MediumWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
140220
? undefined
141221
: t('You must be a member of the project'),
142222
},
223+
{
224+
key: 'copy',
225+
label: t('Copy as markdown'),
226+
onAction: onCopyToClipboard,
227+
tooltip: t('Copy feedback as markdown'),
228+
},
143229
{
144230
key: 'delete',
145231
priority: 'danger' as const,
@@ -156,7 +242,13 @@ function MediumWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
156242
);
157243
}
158244

159-
function SmallWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
245+
function SmallWidth({
246+
feedbackItem,
247+
onCopyToClipboard,
248+
}: {
249+
feedbackItem: FeedbackIssue;
250+
onCopyToClipboard: () => void;
251+
}) {
160252
const {
161253
enableDelete,
162254
onDelete,
@@ -189,6 +281,12 @@ function SmallWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
189281
label: isSpam ? t('Move to Inbox') : t('Mark as Spam'),
190282
onAction: onSpamClick,
191283
},
284+
{
285+
key: 'copy',
286+
label: t('Copy as markdown'),
287+
onAction: onCopyToClipboard,
288+
tooltip: t('Copy feedback as markdown'),
289+
},
192290
{
193291
key: 'read',
194292
label: hasSeen ? t('Mark Unread') : t('Mark Read'),

static/app/utils/analytics/feedbackAnalyticsEvents.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export type FeedbackEventParameters = {
22
'feedback.details-integration-issue-clicked': {
33
integration_key: string;
44
};
5+
'feedback.feedback-item-copy-as-markdown': Record<string, unknown>;
56
'feedback.feedback-item-not-found': {feedbackId: string};
67
'feedback.feedback-item-rendered': Record<string, unknown>;
78
'feedback.index-setup-viewed': Record<string, unknown>;
@@ -28,6 +29,7 @@ export type FeedbackEventParameters = {
2829
type FeedbackEventKey = keyof FeedbackEventParameters;
2930

3031
export const feedbackEventMap: Record<FeedbackEventKey, string | null> = {
32+
'feedback.feedback-item-copy-as-markdown': 'Copied Feedback Item as Markdown',
3133
'feedback.feedback-item-not-found': 'Feedback item not found',
3234
'feedback.feedback-item-rendered': 'Loaded and rendered a feedback item',
3335
'feedback.index-setup-viewed': 'Viewed Feedback Onboarding Setup',

0 commit comments

Comments
 (0)