Skip to content

Commit fab31b9

Browse files
scttcperclaude
andauthored
feat(stacktrace): Wire SCM source context into new stack trace (#111770)
same as #110327 but for the new stack trace. Fetches source content when a flag is enabled and the source is not available already. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 659bef9 commit fab31b9

File tree

8 files changed

+112
-16
lines changed

8 files changed

+112
-16
lines changed

static/app/components/stackTrace/frame/frameContent.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
hasContextRegisters,
1515
} from 'sentry/components/events/interfaces/frame/utils';
1616
import {parseAssembly} from 'sentry/components/events/interfaces/utils';
17+
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
1718
import {FrameVariablesGrid} from 'sentry/components/stackTrace/frame/frameVariablesGrid';
1819
import {
1920
useStackTraceContext,
@@ -31,10 +32,16 @@ const COVERAGE_TEXT: Record<Coverage, string | undefined> = {
3132
};
3233

3334
interface FrameContentProps {
35+
effectiveContext?: Array<[number, string | null]>;
36+
isLoadingSourceContext?: boolean;
3437
sourceLineCoverage?: Array<Coverage | undefined>;
3538
}
3639

37-
export function FrameContent({sourceLineCoverage = []}: FrameContentProps) {
40+
export function FrameContent({
41+
sourceLineCoverage = [],
42+
effectiveContext,
43+
isLoadingSourceContext,
44+
}: FrameContentProps) {
3845
const {event, frame, frameContextId, frameIndex, isExpanded, platform} =
3946
useStackTraceFrameContext();
4047
const {frames, lastFrameIndex, meta, stacktrace} = useStackTraceContext();
@@ -46,7 +53,7 @@ export function FrameContent({sourceLineCoverage = []}: FrameContentProps) {
4653
hasBeenExpandedRef.current = true;
4754
}
4855

49-
const contextLines = isExpanded ? (frame.context ?? []) : [];
56+
const contextLines = isExpanded ? (effectiveContext ?? frame.context ?? []) : [];
5057
const fileExtension = isExpanded ? (getFileExtension(frame.filename ?? '') ?? '') : '';
5158
const prismLines = usePrismTokensSourceContext({
5259
contextLines,
@@ -62,7 +69,11 @@ export function FrameContent({sourceLineCoverage = []}: FrameContentProps) {
6269
const hasFrameVariables = !!frameVariables && Object.keys(frameVariables).length > 0;
6370
const hasFrameRegisters = !!expandedFrameRegisters;
6471
const hasAnyFrameDetails =
65-
hasSourceContext || hasFrameVariables || hasFrameRegisters || hasFrameAssembly;
72+
hasSourceContext ||
73+
isLoadingSourceContext ||
74+
hasFrameVariables ||
75+
hasFrameRegisters ||
76+
hasFrameAssembly;
6677
const shouldShowNoDetails =
6778
frameIndex === lastFrameIndex && frameIndex === 0 && !hasAnyFrameDetails;
6879

@@ -78,7 +89,12 @@ export function FrameContent({sourceLineCoverage = []}: FrameContentProps) {
7889
overflowX="hidden"
7990
data-test-id="core-stacktrace-frame-context"
8091
>
81-
{hasSourceContext ? (
92+
{isLoadingSourceContext ? (
93+
<Container padding="sm md">
94+
<LoadingIndicator mini size={16} />
95+
{t('Loading source context…')}
96+
</Container>
97+
) : hasSourceContext ? (
8298
<FrameSourceGrid>
8399
{contextLines.map(([lineNumber, lineValue], lineIndex) => (
84100
<FrameSourceRow

static/app/components/stackTrace/frame/frameRow.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const StackTraceFrameRowRoot = memo(function StackTraceFrameRowRoot({
2929
const {
3030
event,
3131
frames,
32+
hasScmSourceContext,
3233
lastFrameIndex,
3334
platform,
3435
stacktrace,
@@ -43,6 +44,7 @@ const StackTraceFrameRowRoot = memo(function StackTraceFrameRowRoot({
4344
frame: row.frame,
4445
registers,
4546
platform,
47+
hasScmSourceContext,
4648
});
4749

4850
const frameContextId = useId();

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,38 @@ describe('IssueStackTrace', () => {
664664
});
665665
});
666666

667+
it('fetches and renders SCM source context for frames without embedded context', async () => {
668+
const {event, stacktrace} = makeCopyTestData();
669+
const organization = OrganizationFixture({features: ['scm-source-context']});
670+
ProjectsStore.loadInitialData([ProjectFixture({id: '1', slug: 'project-slug'})]);
671+
672+
const sourceContextRequest = MockApiClient.addMockResponse({
673+
url: '/projects/org-slug/project-slug/stacktrace-source-context/',
674+
body: {context: [[42, 'def handle():']], sourceUrl: null, error: null},
675+
});
676+
677+
render(
678+
<IssueStackTrace
679+
event={event}
680+
values={[
681+
{
682+
type: 'RuntimeError',
683+
value: 'broke',
684+
module: null,
685+
mechanism: {handled: false, type: 'generic'},
686+
stacktrace,
687+
rawStacktrace: null,
688+
threadId: null,
689+
},
690+
]}
691+
/>,
692+
{organization}
693+
);
694+
695+
expect(sourceContextRequest).toHaveBeenCalled();
696+
expect(await screen.findByText('def handle():')).toBeInTheDocument();
697+
});
698+
667699
describe('exception groups', () => {
668700
function makeExceptionGroupValues(): {
669701
event: ReturnType<typeof EventFixture>;

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import type {Group} from 'sentry/types/group';
4040
import type {Project} from 'sentry/types/project';
4141
import type {StacktraceType} from 'sentry/types/stacktrace';
4242
import {defined} from 'sentry/utils';
43+
import {useOrganization} from 'sentry/utils/useOrganization';
4344
import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
4445
import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
4546

@@ -148,6 +149,8 @@ function IssueStackTraceContent({
148149
isStandalone,
149150
}: IssueStackTraceBaseProps & {isStandalone: boolean; values: ExceptionValue[]}) {
150151
const {isMinified, isNewestFirst, view} = useStackTraceViewState();
152+
const organization = useOrganization();
153+
const hasScmSourceContext = organization.features.includes('scm-source-context');
151154
const {hiddenExceptions, toggleRelatedExceptions, expandException} =
152155
useHiddenExceptions(values);
153156

@@ -258,6 +261,7 @@ function IssueStackTraceContent({
258261
<StackTraceProvider
259262
exceptionIndex={isStandalone ? undefined : exc.exceptionIndex}
260263
event={event}
264+
hasScmSourceContext={hasScmSourceContext}
261265
stacktrace={exc.stacktrace}
262266
minifiedStacktrace={exc.rawStacktrace ?? undefined}
263267
meta={isStandalone ? rawEntryMeta : excMeta?.stacktrace}
@@ -344,6 +348,7 @@ function IssueStackTraceContent({
344348
<StackTraceProvider
345349
exceptionIndex={exc.exceptionIndex}
346350
event={event}
351+
hasScmSourceContext={hasScmSourceContext}
347352
stacktrace={exc.stacktrace}
348353
minifiedStacktrace={exc.rawStacktrace ?? undefined}
349354
meta={exceptionValuesMeta?.[exc.exceptionIndex]?.stacktrace}

static/app/components/stackTrace/issueStackTrace/issueStackTraceFrameContext.tsx

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import {useEffect} from 'react';
1+
import {useEffect, useMemo} from 'react';
22

33
import {useLineCoverageContext} from 'sentry/components/events/interfaces/crashContent/exception/lineCoverageContext';
4+
import {useSourceContext} from 'sentry/components/events/interfaces/frame/useSourceContext';
45
import {useStacktraceCoverage} from 'sentry/components/events/interfaces/frame/useStacktraceCoverage';
6+
import {
7+
hasContextSource,
8+
hasPotentialSourceContext,
9+
} from 'sentry/components/events/interfaces/frame/utils';
510
import {FrameContent} from 'sentry/components/stackTrace/frame/frameContent';
611
import {
712
useStackTraceContext,
@@ -25,25 +30,47 @@ function getLineCoverage(
2530

2631
export function IssueStackTraceFrameContext() {
2732
const {event, frame, isExpanded} = useStackTraceFrameContext();
28-
const {project} = useStackTraceContext();
33+
const {hasScmSourceContext, project} = useStackTraceContext();
2934
const {hasCoverageData, setHasCoverageData} = useLineCoverageContext();
30-
const organization = useOrganization({allowNull: true});
35+
const organization = useOrganization();
36+
37+
const hasEmbeddedContext = hasContextSource(frame);
38+
const shouldFetchSourceContext =
39+
hasScmSourceContext &&
40+
defined(project) &&
41+
!hasEmbeddedContext &&
42+
isExpanded &&
43+
hasPotentialSourceContext(frame);
44+
45+
const {data: sourceContextData, isPending: isLoadingSourceContext} = useSourceContext(
46+
{
47+
event,
48+
frame,
49+
orgSlug: organization.slug,
50+
projectSlug: project?.slug,
51+
},
52+
{enabled: shouldFetchSourceContext}
53+
);
3154

32-
const contextLines = isExpanded ? (frame.context ?? []) : [];
55+
const scmContext = useMemo(() => {
56+
if (!sourceContextData?.context?.length) {
57+
return undefined;
58+
}
59+
return sourceContextData.context;
60+
}, [sourceContextData]);
61+
62+
const effectiveContext = hasEmbeddedContext ? frame.context : scmContext;
63+
const contextLines = isExpanded ? (effectiveContext ?? []) : [];
3364

3465
const {data: coverageData, isPending: isLoadingCoverage} = useStacktraceCoverage(
3566
{
3667
event,
3768
frame,
38-
orgSlug: organization?.slug || '',
69+
orgSlug: organization.slug,
3970
projectSlug: project?.slug,
4071
},
4172
{
42-
enabled:
43-
isExpanded &&
44-
defined(organization) &&
45-
defined(project) &&
46-
!!organization.codecovAccess,
73+
enabled: isExpanded && defined(project) && !!organization.codecovAccess,
4774
}
4875
);
4976

@@ -66,5 +93,11 @@ export function IssueStackTraceFrameContext() {
6693
}
6794
}, [coverageData, hasCoverageData, isLoadingCoverage, setHasCoverageData]);
6895

69-
return <FrameContent sourceLineCoverage={sourceLineCoverage} />;
96+
return (
97+
<FrameContent
98+
sourceLineCoverage={sourceLineCoverage}
99+
effectiveContext={effectiveContext}
100+
isLoadingSourceContext={shouldFetchSourceContext && isLoadingSourceContext}
101+
/>
102+
);
70103
}

static/app/components/stackTrace/stackTraceContext.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export interface StackTraceContextValue {
5959
frames: Frame[];
6060
/** True when any visible frame row has expandable details. */
6161
hasAnyExpandableFrames: boolean;
62+
/** True when the SCM source context feature is enabled for this org. */
63+
hasScmSourceContext: boolean;
6264
/** Hidden-system-frame expansion state keyed by frame index. */
6365
hiddenFrameToggleMap: Record<number, boolean>;
6466
/** True when the "Unminify Code" source map action must be hidden. */

static/app/components/stackTrace/stackTraceProvider.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export function StackTraceProvider({
2222
exceptionIndex,
2323
event,
2424
frameSourceMapDebuggerData,
25+
hasScmSourceContext,
2526
hideSourceMapDebugger,
2627
minifiedStacktrace,
2728
stacktrace,
@@ -106,9 +107,10 @@ export function StackTraceProvider({
106107
frame: row.frame,
107108
registers,
108109
platform,
110+
hasScmSourceContext,
109111
});
110112
}),
111-
[rows, frames.length, activeStacktrace.registers, platform]
113+
[rows, frames.length, activeStacktrace.registers, platform, hasScmSourceContext]
112114
);
113115

114116
const toggleHiddenFrames = useCallback((frameIndex: number) => {
@@ -124,6 +126,7 @@ export function StackTraceProvider({
124126
exceptionIndex,
125127
event,
126128
hasAnyExpandableFrames,
129+
hasScmSourceContext: hasScmSourceContext ?? false,
127130
platform,
128131
project,
129132
stacktrace: activeStacktrace,
@@ -143,6 +146,7 @@ export function StackTraceProvider({
143146
frameSourceMapDebuggerData,
144147
frames,
145148
hasAnyExpandableFrames,
149+
hasScmSourceContext,
146150
hideSourceMapDebugger,
147151
hiddenFrameToggleMap,
148152
lastFrameIndex,

static/app/components/stackTrace/types.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export interface StackTraceProviderProps {
6060
exceptionIndex?: number;
6161
/** Per-frame source map debugger data, powering the "Unminify Code" action. */
6262
frameSourceMapDebuggerData?: FrameSourceMapDebuggerData[];
63+
/** Whether the SCM source context feature is enabled for this org. */
64+
hasScmSourceContext?: boolean;
6365
/** Hide the source maps debugger button entirely. */
6466
hideSourceMapDebugger?: boolean;
6567
/** Cap the number of frames rendered. Frames beyond this depth are omitted. */

0 commit comments

Comments
 (0)