diff --git a/static/app/components/stackTrace/frame/frameContent.tsx b/static/app/components/stackTrace/frame/frameContent.tsx index f1419b13a015f3..bac23ded5d2764 100644 --- a/static/app/components/stackTrace/frame/frameContent.tsx +++ b/static/app/components/stackTrace/frame/frameContent.tsx @@ -14,6 +14,7 @@ import { hasContextRegisters, } from 'sentry/components/events/interfaces/frame/utils'; import {parseAssembly} from 'sentry/components/events/interfaces/utils'; +import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {FrameVariablesGrid} from 'sentry/components/stackTrace/frame/frameVariablesGrid'; import { useStackTraceContext, @@ -31,10 +32,16 @@ const COVERAGE_TEXT: Record = { }; interface FrameContentProps { + effectiveContext?: Array<[number, string | null]>; + isLoadingSourceContext?: boolean; sourceLineCoverage?: Array; } -export function FrameContent({sourceLineCoverage = []}: FrameContentProps) { +export function FrameContent({ + sourceLineCoverage = [], + effectiveContext, + isLoadingSourceContext, +}: FrameContentProps) { const {event, frame, frameContextId, frameIndex, isExpanded, platform} = useStackTraceFrameContext(); const {frames, lastFrameIndex, meta, stacktrace} = useStackTraceContext(); @@ -46,7 +53,7 @@ export function FrameContent({sourceLineCoverage = []}: FrameContentProps) { hasBeenExpandedRef.current = true; } - const contextLines = isExpanded ? (frame.context ?? []) : []; + const contextLines = isExpanded ? (effectiveContext ?? frame.context ?? []) : []; const fileExtension = isExpanded ? (getFileExtension(frame.filename ?? '') ?? '') : ''; const prismLines = usePrismTokensSourceContext({ contextLines, @@ -62,7 +69,11 @@ export function FrameContent({sourceLineCoverage = []}: FrameContentProps) { const hasFrameVariables = !!frameVariables && Object.keys(frameVariables).length > 0; const hasFrameRegisters = !!expandedFrameRegisters; const hasAnyFrameDetails = - hasSourceContext || hasFrameVariables || hasFrameRegisters || hasFrameAssembly; + hasSourceContext || + isLoadingSourceContext || + hasFrameVariables || + hasFrameRegisters || + hasFrameAssembly; const shouldShowNoDetails = frameIndex === lastFrameIndex && frameIndex === 0 && !hasAnyFrameDetails; @@ -78,7 +89,12 @@ export function FrameContent({sourceLineCoverage = []}: FrameContentProps) { overflowX="hidden" data-test-id="core-stacktrace-frame-context" > - {hasSourceContext ? ( + {isLoadingSourceContext ? ( + + + {t('Loading source context…')} + + ) : hasSourceContext ? ( {contextLines.map(([lineNumber, lineValue], lineIndex) => ( { }); }); + it('fetches and renders SCM source context for frames without embedded context', async () => { + const {event, stacktrace} = makeCopyTestData(); + const organization = OrganizationFixture({features: ['scm-source-context']}); + ProjectsStore.loadInitialData([ProjectFixture({id: '1', slug: 'project-slug'})]); + + const sourceContextRequest = MockApiClient.addMockResponse({ + url: '/projects/org-slug/project-slug/stacktrace-source-context/', + body: {context: [[42, 'def handle():']], sourceUrl: null, error: null}, + }); + + render( + , + {organization} + ); + + expect(sourceContextRequest).toHaveBeenCalled(); + expect(await screen.findByText('def handle():')).toBeInTheDocument(); + }); + describe('exception groups', () => { function makeExceptionGroupValues(): { event: ReturnType; diff --git a/static/app/components/stackTrace/issueStackTrace/index.tsx b/static/app/components/stackTrace/issueStackTrace/index.tsx index 8d8574524e3fe4..b7ccc126ef61b9 100644 --- a/static/app/components/stackTrace/issueStackTrace/index.tsx +++ b/static/app/components/stackTrace/issueStackTrace/index.tsx @@ -40,6 +40,7 @@ import type {Group} from 'sentry/types/group'; import type {Project} from 'sentry/types/project'; import type {StacktraceType} from 'sentry/types/stacktrace'; import {defined} from 'sentry/utils'; +import {useOrganization} from 'sentry/utils/useOrganization'; import {SectionKey} from 'sentry/views/issueDetails/streamline/context'; import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection'; @@ -148,6 +149,8 @@ function IssueStackTraceContent({ isStandalone, }: IssueStackTraceBaseProps & {isStandalone: boolean; values: ExceptionValue[]}) { const {isMinified, isNewestFirst, view} = useStackTraceViewState(); + const organization = useOrganization(); + const hasScmSourceContext = organization.features.includes('scm-source-context'); const {hiddenExceptions, toggleRelatedExceptions, expandException} = useHiddenExceptions(values); @@ -258,6 +261,7 @@ function IssueStackTraceContent({ { + if (!sourceContextData?.context?.length) { + return undefined; + } + return sourceContextData.context; + }, [sourceContextData]); + + const effectiveContext = hasEmbeddedContext ? frame.context : scmContext; + const contextLines = isExpanded ? (effectiveContext ?? []) : []; const {data: coverageData, isPending: isLoadingCoverage} = useStacktraceCoverage( { event, frame, - orgSlug: organization?.slug || '', + orgSlug: organization.slug, projectSlug: project?.slug, }, { - enabled: - isExpanded && - defined(organization) && - defined(project) && - !!organization.codecovAccess, + enabled: isExpanded && defined(project) && !!organization.codecovAccess, } ); @@ -66,5 +93,11 @@ export function IssueStackTraceFrameContext() { } }, [coverageData, hasCoverageData, isLoadingCoverage, setHasCoverageData]); - return ; + return ( + + ); } diff --git a/static/app/components/stackTrace/stackTraceContext.tsx b/static/app/components/stackTrace/stackTraceContext.tsx index 0501edf2efba9c..a74d4de7b1155f 100644 --- a/static/app/components/stackTrace/stackTraceContext.tsx +++ b/static/app/components/stackTrace/stackTraceContext.tsx @@ -59,6 +59,8 @@ export interface StackTraceContextValue { frames: Frame[]; /** True when any visible frame row has expandable details. */ hasAnyExpandableFrames: boolean; + /** True when the SCM source context feature is enabled for this org. */ + hasScmSourceContext: boolean; /** Hidden-system-frame expansion state keyed by frame index. */ hiddenFrameToggleMap: Record; /** True when the "Unminify Code" source map action must be hidden. */ diff --git a/static/app/components/stackTrace/stackTraceProvider.tsx b/static/app/components/stackTrace/stackTraceProvider.tsx index c19c19de17f91e..3fd3775a5d882f 100644 --- a/static/app/components/stackTrace/stackTraceProvider.tsx +++ b/static/app/components/stackTrace/stackTraceProvider.tsx @@ -22,6 +22,7 @@ export function StackTraceProvider({ exceptionIndex, event, frameSourceMapDebuggerData, + hasScmSourceContext, hideSourceMapDebugger, minifiedStacktrace, stacktrace, @@ -106,9 +107,10 @@ export function StackTraceProvider({ frame: row.frame, registers, platform, + hasScmSourceContext, }); }), - [rows, frames.length, activeStacktrace.registers, platform] + [rows, frames.length, activeStacktrace.registers, platform, hasScmSourceContext] ); const toggleHiddenFrames = useCallback((frameIndex: number) => { @@ -124,6 +126,7 @@ export function StackTraceProvider({ exceptionIndex, event, hasAnyExpandableFrames, + hasScmSourceContext: hasScmSourceContext ?? false, platform, project, stacktrace: activeStacktrace, @@ -143,6 +146,7 @@ export function StackTraceProvider({ frameSourceMapDebuggerData, frames, hasAnyExpandableFrames, + hasScmSourceContext, hideSourceMapDebugger, hiddenFrameToggleMap, lastFrameIndex, diff --git a/static/app/components/stackTrace/types.tsx b/static/app/components/stackTrace/types.tsx index 56f7a38e355044..d7f28c0b5b9e9e 100644 --- a/static/app/components/stackTrace/types.tsx +++ b/static/app/components/stackTrace/types.tsx @@ -60,6 +60,8 @@ export interface StackTraceProviderProps { exceptionIndex?: number; /** Per-frame source map debugger data, powering the "Unminify Code" action. */ frameSourceMapDebuggerData?: FrameSourceMapDebuggerData[]; + /** Whether the SCM source context feature is enabled for this org. */ + hasScmSourceContext?: boolean; /** Hide the source maps debugger button entirely. */ hideSourceMapDebugger?: boolean; /** Cap the number of frames rendered. Frames beyond this depth are omitted. */