Skip to content

Commit 0dc92ed

Browse files
authored
fix(issue details): Correctly copy issue details for threaded stacktraces (#105434)
<!-- Describe your PR here. --> Problem: Current "Copy As Markdown" button in issue details doesn't handle threaded stacktraces and ignores the user selected thread. Solution: Following [this commit](d646b22), added setting and getting of active thread value from ui and included a threads section in the markdown with the relevant stacktrace. Also extracted the formatStacktraceToMarkdown method for easy reuse. Testing: Works locally with both button and hotkeys. Tooltip also updates with correct length based on thread selected. Updated for threading in useCopyIssueDetails.spec.tsx Watchdog thread selected, added portion after change: <img width="382" height="239" alt="Screenshot 2025-12-23 at 12 26 26 PM" src="https://github.com/user-attachments/assets/bda34b30-015e-4398-b600-e1bf3c4b11a1" /> Main thread selected, added portion after change: <img width="558" height="468" alt="Screenshot 2025-12-23 at 12 26 45 PM" src="https://github.com/user-attachments/assets/5ea8a4da-a22c-4930-ad08-2cc87e0dad3d" />
1 parent d4cd486 commit 0dc92ed

File tree

5 files changed

+300
-55
lines changed

5 files changed

+300
-55
lines changed

static/app/components/events/autofix/utils.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export function formatRootCauseWithEvent(
7474
return rootCauseText;
7575
}
7676

77-
const eventText = '\n# Raw Event Data\n' + formatEventToMarkdown(event);
77+
const eventText = '\n# Raw Event Data\n' + formatEventToMarkdown(event, undefined);
7878
return rootCauseText + eventText;
7979
}
8080

@@ -95,7 +95,7 @@ export function formatSolutionWithEvent(
9595
combinedText += solutionText;
9696

9797
if (event) {
98-
const eventText = '\n# Raw Event Data\n' + formatEventToMarkdown(event);
98+
const eventText = '\n# Raw Event Data\n' + formatEventToMarkdown(event, undefined);
9999
combinedText += eventText;
100100
}
101101

static/app/components/events/interfaces/threads.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import type {PlatformKey, Project} from 'sentry/types/project';
3939
import {StackType, StackView} from 'sentry/types/stacktrace';
4040
import {defined} from 'sentry/utils';
4141
import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
42+
import {setActiveThreadId} from 'sentry/views/issueDetails/streamline/hooks/useCopyIssueDetails';
4243
import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
4344
import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
4445

@@ -178,6 +179,11 @@ export function Threads({data, event, projectSlug, groupingCurrentLevel, group}:
178179
const hasStreamlinedUI = useHasStreamlinedUI();
179180
const [activeThread, setActiveThread] = useActiveThreadState(event, threads);
180181

182+
// Sync active thread to module store for copy functionality
183+
useEffect(() => {
184+
setActiveThreadId(activeThread?.id);
185+
}, [activeThread?.id]);
186+
181187
const stackTraceNotFound = !threads.length;
182188

183189
const hasMoreThanOneThread = threads.length > 1;

static/app/views/issueDetails/streamline/eventTitle.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ import {Divider} from 'sentry/views/issueDetails/divider';
2626
import EventCreatedTooltip from 'sentry/views/issueDetails/eventCreatedTooltip';
2727
import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
2828
import {getFoldSectionKey} from 'sentry/views/issueDetails/streamline/foldSection';
29-
import {issueAndEventToMarkdown} from 'sentry/views/issueDetails/streamline/hooks/useCopyIssueDetails';
29+
import {
30+
issueAndEventToMarkdown,
31+
useActiveThreadId,
32+
} from 'sentry/views/issueDetails/streamline/hooks/useCopyIssueDetails';
3033
import {IssueDetailsJumpTo} from 'sentry/views/issueDetails/streamline/issueDetailsJumpTo';
3134

3235
type EventNavigationProps = {
@@ -45,14 +48,21 @@ export const MIN_NAV_HEIGHT = 44;
4548

4649
function GroupMarkdownButton({group, event}: {event: Event; group: Group}) {
4750
const organization = useOrganization();
51+
const activeThreadId = useActiveThreadId();
4852

4953
// Get data for markdown copy functionality
5054
const {data: groupSummaryData} = useGroupSummaryData(group);
5155
const {data: autofixData} = useAutofixData({groupId: group.id});
5256

5357
const markdownText = useMemo(() => {
54-
return issueAndEventToMarkdown(group, event, groupSummaryData, autofixData);
55-
}, [group, event, groupSummaryData, autofixData]);
58+
return issueAndEventToMarkdown(
59+
group,
60+
event,
61+
groupSummaryData,
62+
autofixData,
63+
activeThreadId
64+
);
65+
}, [group, event, groupSummaryData, autofixData, activeThreadId]);
5666
const markdownLines = markdownText.trim().split('\n').length.toLocaleString();
5767

5868
const {copy} = useCopyToClipboard();

static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.spec.tsx

Lines changed: 183 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,21 @@ describe('useCopyIssueDetails', () => {
8989
});
9090

9191
it('formats basic issue information correctly', () => {
92-
const result = issueAndEventToMarkdown(group, event, null, null);
92+
const result = issueAndEventToMarkdown(group, event, null, null, undefined);
9393

9494
expect(result).toContain(`# ${group.title}`);
9595
expect(result).toContain(`**Issue ID:** ${group.id}`);
9696
expect(result).toContain(`**Project:** ${group.project?.slug}`);
9797
});
9898

9999
it('includes group summary data when provided', () => {
100-
const result = issueAndEventToMarkdown(group, event, mockGroupSummaryData, null);
100+
const result = issueAndEventToMarkdown(
101+
group,
102+
event,
103+
mockGroupSummaryData,
104+
null,
105+
undefined
106+
);
101107

102108
expect(result).toContain('## Issue Summary');
103109
expect(result).toContain(mockGroupSummaryData.headline);
@@ -109,7 +115,13 @@ describe('useCopyIssueDetails', () => {
109115
});
110116

111117
it('includes autofix data when provided', () => {
112-
const result = issueAndEventToMarkdown(group, event, null, mockAutofixData);
118+
const result = issueAndEventToMarkdown(
119+
group,
120+
event,
121+
null,
122+
mockAutofixData,
123+
undefined
124+
);
113125

114126
expect(result).toContain('## Root Cause');
115127
expect(result).toContain('## Solution');
@@ -124,7 +136,7 @@ describe('useCopyIssueDetails', () => {
124136
],
125137
};
126138

127-
const result = issueAndEventToMarkdown(group, eventWithTags, null, null);
139+
const result = issueAndEventToMarkdown(group, eventWithTags, null, null, undefined);
128140

129141
expect(result).toContain('## Tags');
130142
expect(result).toContain('**browser:** Chrome');
@@ -162,20 +174,185 @@ describe('useCopyIssueDetails', () => {
162174
],
163175
});
164176

165-
const result = issueAndEventToMarkdown(group, eventWithException, null, null);
177+
const result = issueAndEventToMarkdown(
178+
group,
179+
eventWithException,
180+
null,
181+
null,
182+
undefined
183+
);
166184

167185
expect(result).toContain('## Exception');
168186
expect(result).toContain('**Type:** TypeError');
169187
expect(result).toContain('**Value:** Cannot read property of undefined');
170188
expect(result).toContain('#### Stacktrace');
171189
});
172190

191+
it('includes thread stacktrace when activeThreadId matches', () => {
192+
const eventWithThreads = EventFixture({
193+
...event,
194+
entries: [
195+
{
196+
type: EntryType.THREADS,
197+
data: {
198+
values: [
199+
{
200+
id: 1,
201+
name: 'Main Thread',
202+
crashed: true,
203+
current: true,
204+
stacktrace: {
205+
frames: [
206+
{
207+
function: 'mainFunction',
208+
filename: 'main.py',
209+
lineNo: 10,
210+
inApp: true,
211+
},
212+
],
213+
},
214+
},
215+
{
216+
id: 2,
217+
name: 'Worker Thread',
218+
crashed: false,
219+
current: false,
220+
stacktrace: {
221+
frames: [
222+
{
223+
function: 'workerFunction',
224+
filename: 'worker.py',
225+
lineNo: 25,
226+
inApp: true,
227+
},
228+
],
229+
},
230+
},
231+
],
232+
},
233+
},
234+
],
235+
});
236+
237+
// Pass activeThreadId = 1 to select Main Thread
238+
const result = issueAndEventToMarkdown(group, eventWithThreads, null, null, 1);
239+
240+
expect(result).toContain('## Thread: Main Thread');
241+
expect(result).toContain('(crashed)');
242+
expect(result).toContain('(current)');
243+
expect(result).toContain('mainFunction');
244+
expect(result).toContain('main.py');
245+
expect(result).not.toContain('Worker Thread');
246+
expect(result).not.toContain('workerFunction');
247+
});
248+
249+
it('includes different thread when activeThreadId changes', () => {
250+
const eventWithThreads = EventFixture({
251+
...event,
252+
entries: [
253+
{
254+
type: EntryType.THREADS,
255+
data: {
256+
values: [
257+
{
258+
id: 1,
259+
name: 'Main Thread',
260+
crashed: true,
261+
current: true,
262+
stacktrace: {
263+
frames: [
264+
{
265+
function: 'mainFunction',
266+
filename: 'main.py',
267+
lineNo: 10,
268+
inApp: true,
269+
},
270+
],
271+
},
272+
},
273+
{
274+
id: 2,
275+
name: 'Worker Thread',
276+
crashed: false,
277+
current: false,
278+
stacktrace: {
279+
frames: [
280+
{
281+
function: 'workerFunction',
282+
filename: 'worker.py',
283+
lineNo: 25,
284+
inApp: true,
285+
},
286+
],
287+
},
288+
},
289+
],
290+
},
291+
},
292+
],
293+
});
294+
295+
// Pass activeThreadId = 2 to select Worker Thread
296+
const result = issueAndEventToMarkdown(group, eventWithThreads, null, null, 2);
297+
298+
expect(result).toContain('## Thread: Worker Thread');
299+
expect(result).not.toContain('(crashed)');
300+
expect(result).not.toContain('(current)');
301+
expect(result).toContain('workerFunction');
302+
expect(result).toContain('worker.py');
303+
expect(result).not.toContain('Main Thread');
304+
expect(result).not.toContain('mainFunction');
305+
});
306+
307+
it('does not include thread stacktrace when activeThreadId is undefined', () => {
308+
const eventWithThreads = EventFixture({
309+
...event,
310+
entries: [
311+
{
312+
type: EntryType.THREADS,
313+
data: {
314+
values: [
315+
{
316+
id: 1,
317+
name: 'Main Thread',
318+
crashed: true,
319+
current: true,
320+
stacktrace: {
321+
frames: [
322+
{
323+
function: 'mainFunction',
324+
filename: 'main.py',
325+
lineNo: 10,
326+
inApp: true,
327+
},
328+
],
329+
},
330+
},
331+
],
332+
},
333+
},
334+
],
335+
});
336+
337+
const result = issueAndEventToMarkdown(
338+
group,
339+
eventWithThreads,
340+
null,
341+
null,
342+
undefined
343+
);
344+
345+
expect(result).not.toContain('## Thread');
346+
expect(result).not.toContain('mainFunction');
347+
});
348+
173349
it('prefers autofix rootCause over groupSummary possibleCause', () => {
174350
const result = issueAndEventToMarkdown(
175351
group,
176352
event,
177353
mockGroupSummaryData,
178-
mockAutofixData
354+
mockAutofixData,
355+
undefined
179356
);
180357

181358
expect(result).toContain('## Root Cause');

0 commit comments

Comments
 (0)