Skip to content

Commit 0ded594

Browse files
cvxluocodex
andauthored
feat(inbox): Collapse stack traces when Seer has a root cause (#128263)
When we have seer content, collapse the stacktrace so the timeline is more visible. Co-authored-by: Codex <codex@openai.com>
1 parent 763b502 commit 0ded594

10 files changed

Lines changed: 89 additions & 9 deletions

File tree

‎static/app/components/events/eventStackTrace.tsx‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,15 @@ interface EventStackTraceProps {
2121
event: Event;
2222
group: Group;
2323
projectSlug: Project['slug'];
24+
collapseAll?: boolean;
2425
}
2526

26-
export function EventStackTrace({event, group, projectSlug}: EventStackTraceProps) {
27+
export function EventStackTrace({
28+
event,
29+
group,
30+
projectSlug,
31+
collapseAll,
32+
}: EventStackTraceProps) {
2733
const shouldUseNewStackTrace =
2834
// New stack trace is currently only non-native platforms.
2935
!isNativePlatform(event.platform);
@@ -49,13 +55,15 @@ export function EventStackTrace({event, group, projectSlug}: EventStackTraceProp
4955
<EntryErrorBoundary type={EntryType.EXCEPTION}>
5056
{shouldUseNewStackTrace ? (
5157
<IssueStackTrace
58+
collapseAll={collapseAll}
5259
event={event}
5360
values={eventEntries[EntryType.EXCEPTION].data.values ?? []}
5461
projectSlug={projectSlug}
5562
group={group}
5663
/>
5764
) : (
5865
<Exception
66+
collapseAll={collapseAll}
5967
event={event}
6068
data={eventEntries[EntryType.EXCEPTION].data}
6169
projectSlug={projectSlug}
@@ -70,13 +78,15 @@ export function EventStackTrace({event, group, projectSlug}: EventStackTraceProp
7078
<EntryErrorBoundary type={EntryType.STACKTRACE}>
7179
{shouldUseNewStackTrace ? (
7280
<IssueStackTrace
81+
collapseAll={collapseAll}
7382
event={event}
7483
stacktrace={eventEntries[EntryType.STACKTRACE].data}
7584
projectSlug={projectSlug}
7685
group={group}
7786
/>
7887
) : (
7988
<StackTrace
89+
collapseAll={collapseAll}
8090
event={event}
8191
data={eventEntries[EntryType.STACKTRACE].data}
8292
projectSlug={projectSlug}
@@ -88,6 +98,7 @@ export function EventStackTrace({event, group, projectSlug}: EventStackTraceProp
8898
{defined(eventEntries[EntryType.THREADS]) && (
8999
<EntryErrorBoundary type={EntryType.THREADS}>
90100
<Threads
101+
collapseAll={collapseAll}
91102
event={event}
92103
data={eventEntries[EntryType.THREADS].data}
93104
projectSlug={projectSlug}

‎static/app/components/events/interfaces/crashContent/stackTrace/content.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type DeprecatedLineProps,
77
} from 'sentry/components/events/interfaces/frame/deprecatedLine';
88
import type {FrameSourceMapDebuggerData} from 'sentry/components/events/interfaces/sourceMapsDebuggerModal';
9+
import {useStacktraceContext} from 'sentry/components/events/interfaces/stackTraceContext';
910
import {
1011
getHiddenFrameIndices,
1112
getLastFrameIndex,
@@ -50,6 +51,7 @@ export function Content({
5051
frameSourceMapDebuggerData,
5152
hideSourceMapDebugger = false,
5253
}: Props) {
54+
const {collapseAll} = useStacktraceContext();
5355
const [toggleFrameMap, setToggleFrameMap] = useState(setInitialFrameMap());
5456

5557
const {frames = [], registers} = data;
@@ -138,7 +140,7 @@ export function Content({
138140
const frameProps: DeprecatedLineProps = {
139141
event,
140142
data: frame,
141-
isExpanded: expandFirstFrame && lastFrameIndex === frameIndex,
143+
isExpanded: !collapseAll && expandFirstFrame && lastFrameIndex === frameIndex,
142144
emptySourceNotation: lastFrameIndex === frameIndex && frameIndex === 0,
143145
nextFrame,
144146
platform,

‎static/app/components/events/interfaces/exception.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ type Props = {
2020
event: Event;
2121
group: Group | undefined;
2222
projectSlug: Project['slug'];
23+
collapseAll?: boolean;
2324
groupingCurrentLevel?: Group['metadata']['current_level'];
2425
};
2526

2627
export function Exception({
28+
collapseAll,
2729
event,
2830
data,
2931
projectSlug,
@@ -52,6 +54,7 @@ export function Exception({
5254

5355
return (
5456
<StacktraceContext
57+
collapseAll={collapseAll}
5558
projectSlug={projectSlug}
5659
forceFullStackTrace={hasNonAppFrames ? !data.hasSystemFrames : true}
5760
defaultIsNewestFramesFirst={isStacktraceNewestFirst()}

‎static/app/components/events/interfaces/nativeFrame.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ export function NativeFrame({
103103
const isDartAsyncSuspensionFrame =
104104
frame.filename === '<asynchronous suspension>' ||
105105
frame.absPath === '<asynchronous suspension>';
106-
const {displayOptions, stackView, hasScmSourceContext} = useStacktraceContext();
106+
const {displayOptions, stackView, hasScmSourceContext, collapseAll} =
107+
useStacktraceContext();
107108

108109
const {sectionData} = useIssueDetails();
109110
const debugMetaSearch = useOptionalDebugMetaSearch();
@@ -147,7 +148,7 @@ export function NativeFrame({
147148
defined(frame.function) &&
148149
frame.function !== frame.rawFunction;
149150

150-
const [expanded, setExpanded] = useState(() => isFirstInAppFrame);
151+
const [expanded, setExpanded] = useState(() => !collapseAll && isFirstInAppFrame);
151152
const [isHovering, setHovering] = useState(false);
152153

153154
const contextLine = (frame?.context || []).find(l => l[0] === frame.lineNo);

‎static/app/components/events/interfaces/stackTrace.tsx‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Props = {
1818
data: NonNullable<ExceptionValue['stacktrace']>;
1919
event: Event;
2020
projectSlug: Project['slug'];
21+
collapseAll?: boolean;
2122
groupingCurrentLevel?: Group['metadata']['current_level'];
2223
};
2324

@@ -49,7 +50,13 @@ function StackTraceContentWrapper({
4950
);
5051
}
5152

52-
export function StackTrace({projectSlug, event, data, groupingCurrentLevel}: Props) {
53+
export function StackTrace({
54+
projectSlug,
55+
event,
56+
data,
57+
groupingCurrentLevel,
58+
collapseAll,
59+
}: Props) {
5360
function getPlatform(): PlatformKey {
5461
const framePlatform = data.frames?.find(frame => !!frame.platform);
5562
return framePlatform?.platform ?? event.platform ?? 'other';
@@ -62,6 +69,7 @@ export function StackTrace({projectSlug, event, data, groupingCurrentLevel}: Pro
6269

6370
return (
6471
<StacktraceContext
72+
collapseAll={collapseAll}
6573
projectSlug={projectSlug}
6674
forceFullStackTrace={hasNonAppFrames ? !data.hasSystemFrames : true}
6775
defaultIsNewestFramesFirst={isStacktraceNewestFirst()}

‎static/app/components/events/interfaces/stackTraceContext.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ interface StackTraceContextOptions {
2121
*/
2222
hasSystemFrames: boolean;
2323
projectSlug: string;
24+
collapseAll?: boolean;
2425
/**
2526
* Override the default newest frames first
2627
* @default true
@@ -78,6 +79,7 @@ interface StacktraceContextType {
7879
* Derrived from display options
7980
*/
8081
stackView: StackView;
82+
collapseAll?: boolean;
8183
}
8284

8385
const IssueStacktraceContext = createContext<StacktraceContextType>({
@@ -95,6 +97,7 @@ const IssueStacktraceContext = createContext<StacktraceContextType>({
9597

9698
export function StacktraceContext({
9799
children,
100+
collapseAll = false,
98101
projectSlug,
99102
hasSystemFrames,
100103
forceFullStackTrace = false,
@@ -130,6 +133,7 @@ export function StacktraceContext({
130133

131134
const value = useMemo(
132135
(): StacktraceContextType => ({
136+
collapseAll,
133137
isFullStackTrace: isFullStackTrace || forceFullStackTrace,
134138
setIsFullStackTrace,
135139
isNewestFramesFirst,
@@ -142,6 +146,7 @@ export function StacktraceContext({
142146
forceFullStackTrace,
143147
}),
144148
[
149+
collapseAll,
145150
isFullStackTrace,
146151
forceFullStackTrace,
147152
isNewestFramesFirst,

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type Props = {
5454
group: Group | undefined;
5555
groupingCurrentLevel: Group['metadata']['current_level'];
5656
projectSlug: Project['slug'];
57+
collapseAll?: boolean;
5758
};
5859

5960
function getIntendedStackView(
@@ -167,7 +168,14 @@ function ThreadStackTraceContent({
167168
);
168169
}
169170

170-
export function Threads({data, event, projectSlug, groupingCurrentLevel, group}: Props) {
171+
export function Threads({
172+
data,
173+
event,
174+
projectSlug,
175+
groupingCurrentLevel,
176+
group,
177+
collapseAll,
178+
}: Props) {
171179
// Sort threads by crashed first
172180
const threads = useMemo(
173181
() => (data.values ?? []).toSorted((a, b) => Number(b.crashed) - Number(a.crashed)),
@@ -307,6 +315,7 @@ export function Threads({data, event, projectSlug, groupingCurrentLevel, group}:
307315
</Fragment>
308316
)}
309317
<StacktraceContext
318+
collapseAll={collapseAll}
310319
projectSlug={projectSlug}
311320
forceFullStackTrace={hasNonAppFrames ? stackView === StackView.FULL : true}
312321
defaultIsNewestFramesFirst={isStacktraceNewestFirst()}

‎static/app/components/stackTrace/issueStackTrace/index.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353

5454
interface IssueStackTraceBaseProps {
5555
event: Event;
56+
collapseAll?: boolean;
5657
group?: Group;
5758
projectSlug?: Project['slug'];
5859
}
@@ -76,7 +77,7 @@ type PersistedDisplayOption = 'raw-stack-trace' | 'minified';
7677
const NO_PERSIST_KEY = '__no_persist_stacktrace_display__';
7778

7879
export function IssueStackTrace(props: IssueStackTraceProps) {
79-
const {event, group, projectSlug} = props;
80+
const {event, group, projectSlug, collapseAll} = props;
8081
const organization = useOrganization();
8182
const storageKey = projectSlug
8283
? `issue-details-stracktrace-display-${organization.slug}-${projectSlug}`
@@ -126,6 +127,7 @@ export function IssueStackTrace(props: IssueStackTraceProps) {
126127
>
127128
{projectSlug && <PersistDisplayOptions setPersistedOptions={setPersistedOptions} />}
128129
<IssueStackTraceContent
130+
collapseAll={collapseAll}
129131
// Reset internal state when switching events
130132
key={event.id}
131133
event={event}
@@ -163,6 +165,7 @@ function PersistDisplayOptions({
163165
}
164166

165167
function IssueStackTraceContent({
168+
collapseAll,
166169
event,
167170
values,
168171
group,
@@ -275,6 +278,7 @@ function IssueStackTraceContent({
275278
</ErrorBoundary>
276279
)}
277280
<StackTraceProvider
281+
collapseAll={collapseAll}
278282
exceptionIndex={isStandalone ? undefined : exc.exceptionIndex}
279283
event={event}
280284
hasScmSourceContext={hasScmSourceContext}
@@ -363,6 +367,7 @@ function IssueStackTraceContent({
363367
</ErrorBoundary>
364368
) : null}
365369
<StackTraceProvider
370+
collapseAll={collapseAll}
366371
exceptionIndex={exc.exceptionIndex}
367372
event={event}
368373
hasScmSourceContext={hasScmSourceContext}

‎static/app/views/issueList/pages/inbox/issuePreview/issuePreview.spec.tsx‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ describe('IssuePreview', () => {
198198
const stackTrace = await screen.findByRole('region', {name: 'Stack Trace'});
199199
expect(within(stackTrace).getByText('an error occurred')).toBeVisible();
200200
expect(within(stackTrace).getByText('doThing')).toBeVisible();
201+
expect(within(stackTrace).getByTestId('core-stacktrace-frame-context')).toBeVisible();
201202
expect(summaryRequest).not.toHaveBeenCalled();
202203
});
203204

@@ -309,11 +310,30 @@ describe('IssuePreview', () => {
309310
});
310311
});
311312

312-
it('shows the recommended stack trace alongside Seer analysis', async () => {
313+
it('keeps frames expanded when historical RCA metadata has no Seer content to show', async () => {
314+
MockApiClient.addMockResponse({
315+
url: `/organizations/${organization.slug}/issues/${group.id}/`,
316+
body: {...group, derivedData: fixAppliedGroup.derivedData},
317+
});
313318
MockApiClient.addMockResponse({
314319
url: `/organizations/${organization.slug}/issues/${group.id}/events/recommended/`,
315320
body: EventStacktraceExceptionFixture(),
316321
});
322+
323+
render(<IssuePreview groupId={group.id} />, {organization});
324+
325+
const stackTrace = await screen.findByRole('region', {name: 'Stack Trace'});
326+
expect(within(stackTrace).getByTestId('core-stacktrace-frame-context')).toBeVisible();
327+
});
328+
329+
it('keeps stack trace frames visible with their details collapsed alongside Seer content', async () => {
330+
const event = EventStacktraceExceptionFixture();
331+
const exception = event.entries.find(entry => entry.type === EntryType.EXCEPTION)!;
332+
exception.data.values![0]!.stacktrace!.frames![1]!.context = [[2, 'doThing();']];
333+
MockApiClient.addMockResponse({
334+
url: `/organizations/${organization.slug}/issues/${group.id}/events/recommended/`,
335+
body: event,
336+
});
317337
MockApiClient.addMockResponse({
318338
url: `/organizations/${organization.slug}/issues/${group.id}/autofix/`,
319339
body: ExplorerAutofixResponseFixture(),
@@ -338,6 +358,14 @@ describe('IssuePreview', () => {
338358
);
339359
const stackTrace = await screen.findByRole('region', {name: 'Stack Trace'});
340360
expect(within(stackTrace).getByText('doThing')).toBeVisible();
361+
expect(
362+
within(stackTrace).queryByRole('button', {name: 'View Stack Trace Section'})
363+
).not.toBeInTheDocument();
364+
expect(
365+
within(stackTrace).queryByTestId('core-stacktrace-frame-context')
366+
).not.toBeInTheDocument();
367+
await userEvent.click(within(stackTrace).getByText('doThing'));
368+
expect(within(stackTrace).getByTestId('core-stacktrace-frame-context')).toBeVisible();
341369
await userEvent.click(
342370
within(stackTrace).getByRole('button', {name: 'Display options'})
343371
);

‎static/app/views/issueList/pages/inbox/issuePreview/issuePreviewStackTrace.tsx‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,24 @@ import type {Group} from 'sentry/types/group';
44
import type {Project} from 'sentry/types/project';
55
import {StaticFoldSections} from 'sentry/views/issueDetails/foldSection';
66

7+
import {useIssuePreviewSeer} from './issuePreviewSeer';
8+
79
interface IssuePreviewStackTraceProps {
810
event: Event;
911
group: Group;
1012
projectSlug: Project['slug'];
1113
}
1214

1315
export function IssuePreviewStackTrace(props: IssuePreviewStackTraceProps) {
16+
const {state, autofix} = useIssuePreviewSeer();
17+
const hasSeerContent = state !== 'unavailable' && !!autofix.runState;
1418
return (
1519
<StaticFoldSections>
16-
<EventStackTrace {...props} />
20+
<EventStackTrace
21+
{...props}
22+
key={hasSeerContent ? 'seer' : 'default'}
23+
collapseAll={hasSeerContent}
24+
/>
1725
</StaticFoldSections>
1826
);
1927
}

0 commit comments

Comments
 (0)