From 11a17297d7165d02507c05390dc7cc5097fca424 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Wed, 9 Sep 2026 18:06:31 -0700 Subject: [PATCH 1/2] feat(issues): Use new stack traces across all entry points Route native and non-native events through the shared exception and thread containers. Remove the legacy renderers and unused helpers for the GA experiment, preserving display preferences and ANR annotations. Co-Authored-By: Codex --- .../crashContent/exception/content.spec.tsx | 526 ------- .../crashContent/exception/content.tsx | 411 ----- .../crashContent/exception/index.tsx | 55 - .../crashContent/exception/rawContent.tsx | 117 -- .../exception/relatedExceptions.spec.tsx | 86 -- .../exception/relatedExceptions.tsx | 216 --- .../exception/stackTrace.spec.tsx | 161 -- .../crashContent/exception/stackTrace.tsx | 109 -- .../crashContent/stackTrace/content.spec.tsx | 452 ------ .../crashContent/stackTrace/content.tsx | 220 --- .../crashContent/stackTrace/index.tsx | 76 - .../stackTrace/nativeContent.spec.tsx | 205 --- .../crashContent/stackTrace/nativeContent.tsx | 250 --- .../crashContent/stackTrace/omittedFrames.tsx | 31 - .../events/interfaces/exception.tsx | 127 -- .../events/interfaces/frame/context.spec.tsx | 54 - .../events/interfaces/frame/context.tsx | 251 --- .../interfaces/frame/contextLineNumber.tsx | 45 - .../interfaces/frame/defaultTitle/index.tsx | 258 ---- .../interfaces/frame/deprecatedLine.spec.tsx | 219 --- .../interfaces/frame/deprecatedLine.tsx | 475 ------ .../interfaces/frame/frameVariables.spec.tsx | 210 --- .../interfaces/frame/frameVariables.tsx | 110 -- .../events/interfaces/frame/functionName.tsx | 44 - .../events/interfaces/frame/leadHint.tsx | 23 - .../events/interfaces/frame/utils.tsx | 2 +- .../events/interfaces/nativeFrame.tsx | 579 ------- .../performance/anrRootCause.spec.tsx | 15 +- .../interfaces/performance/anrRootCause.tsx | 21 +- .../events/interfaces/stackTrace.tsx | 104 -- .../interfaces/stackTraceContext.spec.tsx | 126 -- .../events/interfaces/stackTraceContext.tsx | 165 -- .../events/interfaces/template.spec.tsx | 29 + .../components/events/interfaces/template.tsx | 46 +- .../events/interfaces/threads.spec.tsx | 1376 ----------------- .../components/events/interfaces/threads.tsx | 442 ------ .../events/interfaces/utils.spec.tsx | 36 - .../components/events/interfaces/utils.tsx | 46 - .../meta/annotatedText/valueElement.tsx | 4 - .../app/components/events/meta/metaProxy.tsx | 95 -- .../events/traceEventDataSection.tsx | 553 ------- .../stackTracePreview.spec.tsx | 25 +- .../groupPreviewTooltip/stackTracePreview.tsx | 47 +- .../issueStackTrace/anrFrameAction.tsx | 34 + .../issueStackTrace/exceptionStackTrace.tsx | 56 +- .../stackTrace/issueStackTrace/ga.spec.tsx | 156 ++ .../stackTrace/issueStackTrace/index.tsx | 164 +- .../issueStackTrace/issueFrameActions.tsx | 2 + .../sharedIssueStackTrace.spec.tsx | 16 + .../issueStackTrace/sharedIssueStackTrace.tsx | 274 +--- .../issueThreadStackTrace/actions.tsx | 5 +- .../activeThreadModel.ts | 0 .../activeThreadStackTrace.tsx | 59 +- .../issueThreadStackTrace/context.tsx | 11 +- .../issueThreadStackTrace/index.spec.tsx | 2 +- .../issueThreadStackTrace/index.tsx | 3 + .../issueThreadStackTrace/threadSummary.tsx | 0 .../frame/actions/nativeIssueActions.tsx | 2 + .../native/nativeDisplayOptionsContext.tsx | 40 +- .../native/nativeStackTraceProvider.tsx | 12 +- .../stackTrace/stackTraceContext.tsx | 4 +- .../stackTrace/stackTraceFrameList.tsx | 62 + .../stackTrace/stackTraceProvider.tsx | 6 + static/app/components/stackTrace/types.tsx | 4 +- static/app/components/strictClick.tsx | 64 - static/app/constants/index.tsx | 2 - static/app/types/stacktrace.tsx | 11 - .../groupEventDetails/groupEventDetails.tsx | 10 +- .../groupEventDetailsContent.tsx | 84 +- .../details/transaction/sections/entries.tsx | 15 +- .../sharedGroupDetails/sharedEventContent.tsx | 5 +- 71 files changed, 654 insertions(+), 8891 deletions(-) delete mode 100644 static/app/components/events/interfaces/crashContent/exception/content.spec.tsx delete mode 100644 static/app/components/events/interfaces/crashContent/exception/content.tsx delete mode 100644 static/app/components/events/interfaces/crashContent/exception/index.tsx delete mode 100644 static/app/components/events/interfaces/crashContent/exception/rawContent.tsx delete mode 100644 static/app/components/events/interfaces/crashContent/exception/relatedExceptions.spec.tsx delete mode 100644 static/app/components/events/interfaces/crashContent/exception/relatedExceptions.tsx delete mode 100644 static/app/components/events/interfaces/crashContent/exception/stackTrace.spec.tsx delete mode 100644 static/app/components/events/interfaces/crashContent/exception/stackTrace.tsx delete mode 100644 static/app/components/events/interfaces/crashContent/stackTrace/content.spec.tsx delete mode 100644 static/app/components/events/interfaces/crashContent/stackTrace/content.tsx delete mode 100644 static/app/components/events/interfaces/crashContent/stackTrace/index.tsx delete mode 100644 static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.spec.tsx delete mode 100644 static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.tsx delete mode 100644 static/app/components/events/interfaces/crashContent/stackTrace/omittedFrames.tsx delete mode 100644 static/app/components/events/interfaces/exception.tsx delete mode 100644 static/app/components/events/interfaces/frame/context.spec.tsx delete mode 100644 static/app/components/events/interfaces/frame/context.tsx delete mode 100644 static/app/components/events/interfaces/frame/contextLineNumber.tsx delete mode 100644 static/app/components/events/interfaces/frame/defaultTitle/index.tsx delete mode 100644 static/app/components/events/interfaces/frame/deprecatedLine.spec.tsx delete mode 100644 static/app/components/events/interfaces/frame/deprecatedLine.tsx delete mode 100644 static/app/components/events/interfaces/frame/frameVariables.spec.tsx delete mode 100644 static/app/components/events/interfaces/frame/frameVariables.tsx delete mode 100644 static/app/components/events/interfaces/frame/functionName.tsx delete mode 100644 static/app/components/events/interfaces/frame/leadHint.tsx delete mode 100644 static/app/components/events/interfaces/nativeFrame.tsx delete mode 100644 static/app/components/events/interfaces/stackTrace.tsx delete mode 100644 static/app/components/events/interfaces/stackTraceContext.spec.tsx delete mode 100644 static/app/components/events/interfaces/stackTraceContext.tsx create mode 100644 static/app/components/events/interfaces/template.spec.tsx delete mode 100644 static/app/components/events/interfaces/threads.spec.tsx delete mode 100644 static/app/components/events/interfaces/threads.tsx delete mode 100644 static/app/components/events/meta/metaProxy.tsx delete mode 100644 static/app/components/events/traceEventDataSection.tsx create mode 100644 static/app/components/stackTrace/issueStackTrace/anrFrameAction.tsx create mode 100644 static/app/components/stackTrace/issueStackTrace/ga.spec.tsx rename static/app/components/stackTrace/{native => }/issueThreadStackTrace/actions.tsx (96%) rename static/app/components/stackTrace/{native => }/issueThreadStackTrace/activeThreadModel.ts (100%) rename static/app/components/stackTrace/{native => }/issueThreadStackTrace/activeThreadStackTrace.tsx (68%) rename static/app/components/stackTrace/{native => }/issueThreadStackTrace/context.tsx (94%) rename static/app/components/stackTrace/{native => }/issueThreadStackTrace/index.spec.tsx (99%) rename static/app/components/stackTrace/{native => }/issueThreadStackTrace/index.tsx (96%) rename static/app/components/stackTrace/{native => }/issueThreadStackTrace/threadSummary.tsx (100%) create mode 100644 static/app/components/stackTrace/stackTraceFrameList.tsx delete mode 100644 static/app/components/strictClick.tsx diff --git a/static/app/components/events/interfaces/crashContent/exception/content.spec.tsx b/static/app/components/events/interfaces/crashContent/exception/content.spec.tsx deleted file mode 100644 index fd306ae61ec3..000000000000 --- a/static/app/components/events/interfaces/crashContent/exception/content.spec.tsx +++ /dev/null @@ -1,526 +0,0 @@ -import {DataScrubbingRelayPiiConfigFixture} from 'sentry-fixture/dataScrubbingRelayPiiConfig'; -import {EventFixture} from 'sentry-fixture/event'; -import { - EventEntryChainedExceptionFixture, - EventEntryExceptionGroupFixture, -} from 'sentry-fixture/eventEntryChainedException'; -import {EventStacktraceFrameFixture} from 'sentry-fixture/eventStacktraceFrame'; -import {GitHubIntegrationFixture} from 'sentry-fixture/githubIntegration'; -import {OrganizationFixture} from 'sentry-fixture/organization'; -import {DetailedProjectFixture} from 'sentry-fixture/project'; -import {RepositoryFixture} from 'sentry-fixture/repository'; -import {RepositoryProjectPathConfigFixture} from 'sentry-fixture/repositoryProjectPathConfig'; - -import {initializeOrg} from 'sentry-test/initializeOrg'; -import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; -import {textWithMarkupMatcher} from 'sentry-test/utils'; - -import {Content} from 'sentry/components/events/interfaces/crashContent/exception/content'; -import {ProjectsStore} from 'sentry/stores/projectsStore'; -import {EntryType} from 'sentry/types/event'; -import {StackType, StackView} from 'sentry/types/stacktrace'; - -describe('Exception Content', () => { - const organization = OrganizationFixture(); - const project = DetailedProjectFixture(); - const integration = GitHubIntegrationFixture(); - const repo = RepositoryFixture({integrationId: integration.id}); - const config = RepositoryProjectPathConfigFixture({project, repo, integration}); - - beforeEach(() => { - MockApiClient.clearMockResponses(); - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/prompts-activity/`, - }); - MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/stacktrace-link/`, - body: {config, sourceUrl: 'https://something.io', integrations: [integration]}, - }); - ProjectsStore.loadInitialData([project]); - }); - - it('display redacted values from exception entry', async () => { - const projectDetails = DetailedProjectFixture({ - ...project, - relayPiiConfig: JSON.stringify(DataScrubbingRelayPiiConfigFixture()), - }); - MockApiClient.addMockResponse({ - url: `/projects/org-slug/${project.slug}/`, - body: projectDetails, - }); - - const {organization: org} = initializeOrg({ - projects: [project], - }); - - const event = EventFixture({ - _meta: { - entries: { - 0: { - data: { - values: { - '0': { - mechanism: { - data: { - relevant_address: { - '': { - rem: [['project:0', 's', 0, 0]], - len: 43, - }, - }, - }, - }, - value: { - '': { - rem: [['project:0', 's', 0, 0]], - len: 43, - }, - }, - }, - }, - }, - }, - }, - }, - entries: [ - { - type: EntryType.EXCEPTION, - data: { - values: [ - { - mechanism: { - type: 'celery', - handled: false, - data: {relevant_address: null}, - }, - module: 'sentry.models.organization', - rawStacktrace: null, - stacktrace: { - frames: [ - { - function: null, - colNo: null, - vars: {}, - symbol: null, - module: '', - lineNo: null, - package: null, - absPath: - 'https://sentry.io/hiventy/kraken-prod/issues/438681831/?referrer=slack#', - inApp: false, - instructionAddr: null, - filename: '/hiventy/kraken-prod/issues/438681831/', - platform: null, - context: [], - symbolAddr: null, - }, - ], - framesOmitted: null, - registers: null, - hasSystemFrames: false, - }, - threadId: null, - type: 'Organization.DoesNotExist', - value: null, - }, - ], - }, - }, - ], - }); - - render( - , - { - organization: org, - initialRouterConfig: { - location: { - pathname: `/organizations/${org.slug}/issues/`, - query: {project: project.id}, - }, - route: '/organizations/:orgId/issues/', - }, - } - ); - - expect(screen.getAllByText(/redacted/)).toHaveLength(2); - - await userEvent.hover(screen.getAllByText(/redacted/)[0]!); - - expect( - await screen.findByText( - textWithMarkupMatcher( - 'Replaced because of the data scrubbing rule [Replace] [Password fields] with [Scrubbed] from [password] in the settings of the project project-slug' - ) - ) - ).toBeInTheDocument(); // tooltip description - - expect( - screen.getByRole('link', { - name: '[Replace] [Password fields] with [Scrubbed] from [password]', - }) - ).toHaveAttribute( - 'href', - '/settings/org-slug/projects/project-slug/security-and-privacy/advanced-data-scrubbing/0/' - ); - - expect(screen.getByRole('link', {name: 'project-slug'})).toHaveAttribute( - 'href', - '/settings/org-slug/projects/project-slug/security-and-privacy/' - ); - }); - - it('respects platform overrides in stacktrace frames', () => { - const event = EventFixture({ - projectID: project.id, - platform: 'python', - entries: [ - { - type: EntryType.EXCEPTION, - data: { - values: [ - { - stacktrace: { - frames: [EventStacktraceFrameFixture({platform: null})], - }, - }, - { - stacktrace: { - frames: [EventStacktraceFrameFixture({platform: 'cocoa'})], - }, - }, - ], - }, - }, - ], - }); - - render( - , - { - initialRouterConfig: { - location: { - pathname: `/organizations/${organization.slug}/issues/`, - query: {}, - }, - route: '/organizations/:orgId/issues/', - }, - } - ); - - // Cocoa override should render a native stack trace component - expect(screen.getByTestId('native-stack-trace-content')).toBeInTheDocument(); - - // Other stacktrace should render the normal stack trace (python) - expect(screen.getByTestId('stack-trace-content')).toBeInTheDocument(); - }); - - it('does not use fold section for non-chained exceptions', () => { - const event = EventFixture({ - projectID: project.id, - entries: [ - { - type: EntryType.EXCEPTION, - data: { - excOmitted: null, - hasSystemFrames: false, - values: [ - { - type: 'ValueError', - value: 'test', - mechanism: { - handled: true, - type: '', - }, - stacktrace: { - framesOmitted: null, - hasSystemFrames: false, - registers: null, - frames: [ - { - function: 'func4', - module: 'helpers', - filename: 'file4.py', - absPath: 'file4.py', - lineNo: 50, - colNo: null, - context: [[50, 'raise ValueError("test")']], - inApp: true, - rawFunction: null, - package: null, - platform: null, - instructionAddr: null, - symbol: null, - symbolAddr: null, - trust: null, - vars: null, - }, - ], - }, - module: 'helpers', - threadId: null, - rawStacktrace: null, - }, - ], - }, - }, - ], - }); - render( - , - { - initialRouterConfig: { - location: { - pathname: `/organizations/${organization.slug}/issues/`, - query: {}, - }, - route: '/organizations/:orgId/issues/', - }, - } - ); - - expect(screen.getByRole('heading', {name: 'ValueError'})).toBeInTheDocument(); - expect( - screen.queryByRole('button', {name: 'Collapse Section'}) - ).not.toBeInTheDocument(); - expect(screen.queryByRole('button', {name: 'View Section'})).not.toBeInTheDocument(); - }); - - describe('exception groups', () => { - const event = EventFixture({ - entries: [EventEntryExceptionGroupFixture()], - projectID: project.id, - }); - - beforeEach(() => { - MockApiClient.clearMockResponses(); - - const promptResponse = { - dismissed_ts: undefined, - snoozed_ts: undefined, - }; - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/prompts-activity/`, - body: promptResponse, - }); - MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/stacktrace-link/`, - body: {config, sourceUrl: 'https://something.io', integrations: [integration]}, - }); - ProjectsStore.loadInitialData([project]); - }); - - const defaultProps = { - type: StackType.ORIGINAL, - newestFirst: true, - platform: 'python' as const, - stackView: StackView.APP, - event, - values: event.entries[0]!.data.values, - projectSlug: project.slug, - }; - - it('displays exception group tree under first exception', () => { - render(, { - initialRouterConfig: { - location: { - pathname: `/organizations/${organization.slug}/issues/`, - query: {}, - }, - route: '/organizations/:orgId/issues/', - }, - }); - - expect( - screen.getByText('There are 4 chained exceptions in this event.') - ).toBeInTheDocument(); - const exceptions = screen.getAllByTestId('exception-value'); - - // First exception should be the parent ExceptionGroup - expect(within(exceptions[0]!).getByText('ExceptionGroup 1')).toBeInTheDocument(); - expect( - within(exceptions[0]!).getByRole('heading', {name: 'ExceptionGroup 1'}) - ).toBeInTheDocument(); - expect(within(exceptions[0]!).getByText('Related Exceptions')).toBeInTheDocument(); - }); - - it('displays exception group tree in first frame when there is no other context', () => { - render(, { - initialRouterConfig: { - location: { - pathname: `/organizations/${organization.slug}/issues/`, - query: {}, - }, - route: '/organizations/:orgId/issues/', - }, - }); - - const exceptions = screen.getAllByTestId('exception-value'); - - const exceptionGroupWithNoContext = exceptions[2]!; - expect( - within(exceptionGroupWithNoContext).getByText('Related Exceptions') - ).toBeInTheDocument(); - }); - - it('hides sub-groups by default', async () => { - render(, { - initialRouterConfig: { - location: { - pathname: `/organizations/${organization.slug}/issues/`, - query: {}, - }, - route: '/organizations/:orgId/issues/', - }, - }); - - // There are 4 values, but 1 should be hidden - expect(screen.getAllByTestId('exception-value')).toHaveLength(3); - expect(screen.queryByRole('heading', {name: 'ValueError'})).not.toBeInTheDocument(); - - await userEvent.click( - screen.getByRole('button', {name: /show 1 related exception/i}) - ); - - // After expanding, ValueError should be visible - expect(screen.getAllByTestId('exception-value')).toHaveLength(4); - expect(screen.getByRole('heading', {name: 'ValueError'})).toBeInTheDocument(); - - await userEvent.click( - screen.getByRole('button', {name: /hide 1 related exception/i}) - ); - - // After collapsing, ValueError should be gone again - expect(screen.getAllByTestId('exception-value')).toHaveLength(3); - expect(screen.queryByRole('heading', {name: 'ValueError'})).not.toBeInTheDocument(); - }); - - it('auto-opens sub-groups when clicking link in tree', async () => { - render(, { - initialRouterConfig: { - location: { - pathname: `/organizations/${organization.slug}/issues/`, - query: {}, - }, - route: '/organizations/:orgId/issues/', - }, - }); - - expect(screen.queryByRole('heading', {name: 'ValueError'})).not.toBeInTheDocument(); - - expect( - screen.getByRole('button', {name: /show 1 related exception/i}) - ).toBeInTheDocument(); - await userEvent.click(screen.getByRole('button', {name: /ValueError: test/i})); - - // After expanding, ValueError should be visible - expect(screen.getByRole('heading', {name: 'ValueError'})).toBeInTheDocument(); - }); - }); - - describe('chained exceptions', () => { - const event = EventFixture({ - entries: [EventEntryChainedExceptionFixture()], - projectID: project.id, - }); - - beforeEach(() => { - MockApiClient.clearMockResponses(); - - const promptResponse = { - dismissed_ts: undefined, - snoozed_ts: undefined, - }; - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/prompts-activity/`, - body: promptResponse, - }); - MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/stacktrace-link/`, - body: {config, sourceUrl: 'https://something.io', integrations: [integration]}, - }); - ProjectsStore.loadInitialData([project]); - }); - - const defaultProps = { - type: StackType.ORIGINAL, - newestFirst: true, - platform: 'python' as const, - stackView: StackView.APP, - event, - values: event.entries[0]!.data.values, - projectSlug: project.slug, - }; - - it('only expands the first 3 exceptions by default', () => { - render(, { - initialRouterConfig: { - location: { - pathname: `/organizations/${organization.slug}/issues/`, - query: {}, - }, - route: '/organizations/:orgId/issues/', - }, - }); - - // both toggle headings are visible because they are not exception group chained exceptions - expect(screen.getByRole('heading', {name: 'ValueError'})).toBeInTheDocument(); - expect(screen.getByRole('heading', {name: 'TypeError'})).toBeInTheDocument(); - expect(screen.getByRole('heading', {name: 'RuntimeError'})).toBeInTheDocument(); - expect(screen.getByRole('heading', {name: 'SyntaxError'})).toBeInTheDocument(); - - // only ValueError is expanded by default - expect(screen.getAllByRole('button', {name: 'Collapse Section'})).toHaveLength(3); - expect(screen.getAllByRole('button', {name: 'View Section'})).toHaveLength(1); - - // does not show exception group UI elements - expect(screen.queryByText('Related Exceptions')).not.toBeInTheDocument(); - }); - - it('can expand and collapse all exceptions', async () => { - render(, { - initialRouterConfig: { - location: { - pathname: `/organizations/${organization.slug}/issues/`, - query: {}, - }, - route: '/organizations/:orgId/issues/', - }, - }); - - const collapseButtons = screen.getAllByRole('button', {name: 'Collapse Section'}); - for (const button of collapseButtons) { - await userEvent.click(button); - } - - // all exceptions are collapsed - expect( - screen.queryByRole('button', {name: 'Collapse Section'}) - ).not.toBeInTheDocument(); - expect(screen.getAllByRole('button', {name: 'View Section'})).toHaveLength(4); - }); - }); -}); diff --git a/static/app/components/events/interfaces/crashContent/exception/content.tsx b/static/app/components/events/interfaces/crashContent/exception/content.tsx deleted file mode 100644 index 5e5fe89e1309..000000000000 --- a/static/app/components/events/interfaces/crashContent/exception/content.tsx +++ /dev/null @@ -1,411 +0,0 @@ -import {Fragment, useState} from 'react'; -import styled from '@emotion/styled'; - -import {Button} from '@sentry/scraps/button'; -import {Container} from '@sentry/scraps/layout'; -import {Tooltip} from '@sentry/scraps/tooltip'; - -import {ErrorBoundary} from 'sentry/components/errorBoundary'; -import {StacktraceBanners} from 'sentry/components/events/interfaces/crashContent/exception/banners/stacktraceBanners'; -import { - prepareSourceMapDebuggerFrameInformation, - useSourceMapDebugQuery, - type SourceMapDebugResponse, -} from 'sentry/components/events/interfaces/crashContent/exception/useSourceMapDebuggerData'; -import {renderLinksInText} from 'sentry/components/events/interfaces/crashContent/exception/utils'; -import {getStacktracePlatform} from 'sentry/components/events/interfaces/utils'; -import {AnnotatedText} from 'sentry/components/events/meta/annotatedText'; -import {tct, tn} from 'sentry/locale'; -import type {Event, ExceptionType, ExceptionValue} from 'sentry/types/event'; -import type {Project} from 'sentry/types/project'; -import {StackType} from 'sentry/types/stacktrace'; -import {defined} from 'sentry/utils/defined'; -import {useRouteAnalyticsParams} from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams'; -import {useProjects} from 'sentry/utils/useProjects'; -import {SectionKey} from 'sentry/views/issueDetails/context'; -import {FoldSection, SectionDivider} from 'sentry/views/issueDetails/foldSection'; -import {useIsSampleEvent} from 'sentry/views/issueDetails/utils'; - -import {Mechanism} from './mechanism'; -import {RelatedExceptions} from './relatedExceptions'; -import {StackTrace} from './stackTrace'; - -type StackTraceProps = React.ComponentProps; - -type Props = { - event: Event; - newestFirst: boolean; - projectSlug: Project['slug']; - type: StackType; - values: ExceptionType['values']; - meta?: Record; - stackView?: StackTraceProps['stackView']; - threadId?: number; -} & Pick, 'groupingCurrentLevel'>; - -type ExceptionRenderStateMap = Record; - -/** - * We want to hide all children of an exception group is the exception group - * itself is not the root level exception. - */ -const useHiddenExceptions = (values?: ExceptionValue[]) => { - // map of exception group ids and whether their children are hidden - const [hiddenExceptions, setHiddenExceptions] = useState( - () => { - if (!values) { - return {}; - } - - return values - .filter( - ({mechanism}) => mechanism?.is_exception_group && defined(mechanism.parent_id) - ) - .reduce( - (acc, next) => ({...acc, [next.mechanism?.exception_id ?? -1]: true}), - {} - ); - } - ); - - const toggleRelatedExceptions = (exceptionId: number) => { - setHiddenExceptions(old => { - if (!defined(old[exceptionId])) { - return old; - } - - return {...old, [exceptionId]: !old[exceptionId]}; - }); - }; - - const expandException = (exceptionId: number) => { - setHiddenExceptions(old => { - const exceptionValue = values?.find( - value => value.mechanism?.exception_id === exceptionId - ); - const exceptionGroupId = exceptionValue?.mechanism?.parent_id; - if (!defined(exceptionGroupId) || !defined(old[exceptionGroupId])) { - return old; - } - - return {...old, [exceptionGroupId]: false}; - }); - }; - - return { - toggleRelatedExceptions, - hiddenExceptions, - expandException, - }; -}; - -function ToggleExceptionButton({ - values, - exception, - toggleRelatedExceptions, - hiddenExceptions, -}: { - exception: ExceptionValue; - hiddenExceptions: ExceptionRenderStateMap; - toggleRelatedExceptions: (exceptionId: number) => void; - values: ExceptionValue[]; -}) { - const exceptionId = exception.mechanism?.exception_id; - - if (!defined(exceptionId) || !defined(hiddenExceptions[exceptionId])) { - return null; - } - - const collapsed = hiddenExceptions[exceptionId]; - const numChildren = values.filter( - ({mechanism}) => mechanism?.parent_id === exceptionId - ).length; - - return ( - { - toggleRelatedExceptions(exceptionId); - }} - > - {collapsed - ? tn('Show %s related exception', 'Show %s related exceptions', numChildren) - : tn('Hide %s related exception', 'Hide %s related exceptions', numChildren)} - - ); -} - -function InnerContent({ - exception, - exceptionIdx, - project, - event, - meta, - newestFirst, - values, - threadId, - type, - hasChainedExceptions, - sourceMapDebuggerData, - isSampleError, - stackView, - groupingCurrentLevel, - hiddenExceptions, - toggleRelatedExceptions, - expandException, -}: { - exception: ExceptionValue; - exceptionIdx: number; - expandException: (exceptionId: number) => void; - hasChainedExceptions: boolean; - hiddenExceptions: ExceptionRenderStateMap; - isSampleError: boolean; - sourceMapDebuggerData: SourceMapDebugResponse | undefined; - toggleRelatedExceptions: (exceptionId: number) => void; - values: ExceptionValue[]; - project?: Project; -} & Omit) { - const frameSourceMapDebuggerData = sourceMapDebuggerData?.exceptions[ - exceptionIdx - ]!.frames.map(debuggerFrame => - prepareSourceMapDebuggerFrameInformation( - sourceMapDebuggerData, - debuggerFrame, - event, - project?.platform - ) - ); - const exceptionValue = - type === StackType.ORIGINAL ? exception.value : exception.rawValue || exception.value; - - const renderedExceptionValue = exceptionValue - ? renderLinksInText({exceptionText: exceptionValue}) - : null; - const platform = getStacktracePlatform(event, exception.stacktrace); - - // The banners should appear on the top exception only - const isTopException = newestFirst - ? exceptionIdx === values.length - 1 - : exceptionIdx === 0; - - return ( - - - {meta?.[exceptionIdx]?.value?.[''] && !exceptionValue ? ( - - ) : ( - renderedExceptionValue - )} - - - {exception.mechanism ? ( - - - - ) : null} - - {exception.stacktrace && isTopException && !isSampleError && ( - - - - )} - - - ); -} - -export function Content({ - newestFirst, - event, - stackView, - groupingCurrentLevel, - projectSlug, - values, - type, - meta, - threadId, -}: Props) { - const {projects} = useProjects({slugs: [projectSlug]}); - - const {data: sourceMapDebuggerData} = useSourceMapDebugQuery( - projectSlug, - event.id, - event.sdk?.name ?? null - ); - const {hiddenExceptions, toggleRelatedExceptions, expandException} = - useHiddenExceptions(values); - - const isSampleError = useIsSampleEvent(); - - useRouteAnalyticsParams({ - num_exceptions: values?.length ?? 0, - }); - - // Organization context may be unavailable for the shared event view, so we need - // to account for this possibility if we rely on the `useOrganization` hook. - if (!values) { - return null; - } - - const project = projects.find(({slug}) => slug === projectSlug); - const hasChainedExceptions = values.length > 1; - - const children = values.map((exc, excIdx) => { - const id = defined(exc.mechanism?.exception_id) - ? `exception-${exc.mechanism?.exception_id}` - : undefined; - - if ( - exc.mechanism?.parent_id !== undefined && - hiddenExceptions[exc.mechanism.parent_id] - ) { - // hide all child exceptions when the parent - // does not have related exceptions toggled to show - return null; - } - - const innerContent = ( - - ); - - const exceptionType = - type === StackType.ORIGINAL ? exc.type : exc.rawType || exc.type; - const exceptionModule = - type === StackType.ORIGINAL ? exc.module : exc.rawModule || exc.module; - if (hasChainedExceptions) { - return ( - - {exceptionType} - - ) : ( - {exceptionType} - ) - } - disableCollapsePersistence - initialCollapse={excIdx < values.length - 3} - additionalIdentifier={ - exc.mechanism?.exception_id?.toString() ?? excIdx.toString() - } - > - {innerContent} - - ); - } - - return ( -
- {defined(exceptionModule) ? ( - - {exceptionType} - - ) : ( - {exceptionType} - )} - {innerContent} -
- ); - }); - - if (newestFirst) { - children.reverse(); - } - - return ( -
- {hasChainedExceptions && ( - -

- {tct('There are [numExceptions] chained exceptions in this event.', { - numExceptions: values.length, - })} -

- -
- )} - {children} -
- ); -} - -const StyledPre = styled('pre')` - padding: 0; - margin: 0; - word-wrap: break-word; - white-space: pre-wrap; - background-color: inherit; -`; - -const Title = styled('h5')` - margin-bottom: 0; - overflow-wrap: break-word; - word-wrap: break-word; - word-break: break-word; -`; - -const ShowRelatedExceptionsButton = styled(Button)` - font-family: ${p => p.theme.font.family.mono}; - font-size: ${p => p.theme.font.size.sm}; -`; - -const StyledFoldSection = styled(FoldSection)` - margin-bottom: 0; - margin-left: -${p => p.theme.space.sm}; - - & ~ hr { - margin-left: ${p => p.theme.space.xl}; - margin-right: ${p => p.theme.space.xl}; - } -`; diff --git a/static/app/components/events/interfaces/crashContent/exception/index.tsx b/static/app/components/events/interfaces/crashContent/exception/index.tsx deleted file mode 100644 index cd2c709321e2..000000000000 --- a/static/app/components/events/interfaces/crashContent/exception/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import {ErrorBoundary} from 'sentry/components/errorBoundary'; -import {useStacktraceContext} from 'sentry/components/events/interfaces/stackTraceContext'; -import type {Event, ExceptionType} from 'sentry/types/event'; -import type {Group} from 'sentry/types/group'; -import type {Project} from 'sentry/types/project'; -import {StackView} from 'sentry/types/stacktrace'; - -import {Content} from './content'; -import {RawContent} from './rawContent'; - -type Props = { - event: Event; - projectSlug: Project['slug']; - values: ExceptionType['values']; - groupingCurrentLevel?: Group['metadata']['current_level']; - meta?: Record; - threadId?: number; -}; - -export function ExceptionContent({ - projectSlug, - values, - event, - groupingCurrentLevel, - meta, - threadId, -}: Props) { - const {stackView, stackType, isNewestFramesFirst} = useStacktraceContext(); - return ( - - {stackView === StackView.RAW ? ( - - ) : ( - - )} - - ); -} diff --git a/static/app/components/events/interfaces/crashContent/exception/rawContent.tsx b/static/app/components/events/interfaces/crashContent/exception/rawContent.tsx deleted file mode 100644 index d3386dcee674..000000000000 --- a/static/app/components/events/interfaces/crashContent/exception/rawContent.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import {Fragment} from 'react'; -import {useQuery} from '@tanstack/react-query'; - -import {ClippedBox} from 'sentry/components/clippedBox'; -import {displayRawContent as rawStacktraceContent} from 'sentry/components/events/interfaces/crashContent/stackTrace/rawContent'; -import {LoadingError} from 'sentry/components/loadingError'; -import {Placeholder} from 'sentry/components/placeholder'; -import type {Event, ExceptionType} from 'sentry/types/event'; -import type {PlatformKey} from 'sentry/types/platform'; -import type {Project} from 'sentry/types/project'; -import {apiOptions} from 'sentry/utils/api/apiOptions'; -import {useOrganization} from 'sentry/utils/useOrganization'; - -interface Props { - eventId: Event['id']; - platform: PlatformKey | undefined; - projectSlug: Project['slug']; - threadId: number | undefined; - type: 'original' | 'minified'; - values: ExceptionType['values']; -} - -const appleCrashReportPlatforms: PlatformKey[] = [ - 'native', - 'cocoa', - 'nintendo-switch', - 'playstation', - 'xbox', -]; - -export function RawContent({ - eventId, - projectSlug, - type, - platform, - values, - threadId, -}: Props) { - const organization = useOrganization(); - - const isNative = !!platform && appleCrashReportPlatforms.includes(platform); - - const { - data: crashReport, - isPending, - isError, - } = useQuery({ - ...apiOptions.as()( - // Note that this endpoint does not have a trailing slash for some reason - '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/apple-crash-report', - { - path: { - organizationIdOrSlug: organization.slug, - projectIdOrSlug: projectSlug, - eventId, - }, - query: { - minified: String(type === 'minified'), - ...(threadId !== undefined && {thread_id: String(threadId)}), - }, - headers: {Accept: '*/*; charset=utf-8'}, - staleTime: Infinity, - } - ), - enabled: isNative, - }); - - if (isPending && isNative) { - return ; - } - - if (isError) { - return ; - } - - if (!values) { - return null; - } - - return ( - - {values.map((exc, excIdx) => { - if (!isNative) { - const exceptionValue = - type === 'original' ? exc.value : exc.rawValue || exc.value; - const exceptionType = type === 'original' ? exc.type : exc.rawType || exc.type; - - const nonNativeContent = exc.stacktrace ? ( - rawStacktraceContent({ - data: type === 'original' ? exc.stacktrace : exc.rawStacktrace, - platform, - exception: exc, - isMinified: type === 'minified', - }) - ) : ( -
- {exceptionType}: {exceptionValue} -
- ); - return ( -
-
{nonNativeContent}
-
- ); - } - - return ( -
-
-              {crashReport}
-            
-
- ); - })} -
- ); -} diff --git a/static/app/components/events/interfaces/crashContent/exception/relatedExceptions.spec.tsx b/static/app/components/events/interfaces/crashContent/exception/relatedExceptions.spec.tsx deleted file mode 100644 index d513a39faef1..000000000000 --- a/static/app/components/events/interfaces/crashContent/exception/relatedExceptions.spec.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import {EventEntryExceptionGroupFixture} from 'sentry-fixture/eventEntryChainedException'; - -import {render, screen, within} from 'sentry-test/reactTestingLibrary'; - -import {RelatedExceptions} from 'sentry/components/events/interfaces/crashContent/exception/relatedExceptions'; - -describe('ExceptionGroupContext', () => { - const entry = EventEntryExceptionGroupFixture(); - - const exceptionGroup1Mechanism = entry.data.values?.find( - ({type}) => type === 'ExceptionGroup 1' - )?.mechanism; - const exceptionGroup2Mechanism = entry.data.values?.find( - ({type}) => type === 'ExceptionGroup 2' - )?.mechanism; - const typeErrorMechanism = entry.data.values?.find( - ({type}) => type === 'TypeError' - )?.mechanism; - - const defaultProps = { - allExceptions: entry.data.values ?? [], - mechanism: exceptionGroup1Mechanism, - newestFirst: true, - onExceptionClick: jest.fn(), - }; - - it('renders tree with exception group', () => { - render(); - - const items = screen.getAllByTestId('exception-tree-item'); - expect(items).toHaveLength(3); - - // ExceptionGroup should not link to itself - expect(within(items[0]!).getByText('ExceptionGroup 1: parent')).toBeInTheDocument(); - // Should have a link to TypeError exception - expect( - within(items[1]!).getByRole('button', {name: 'TypeError: nested'}) - ).toBeInTheDocument(); - // Should have a link to child exception group - expect( - within(items[2]!).getByRole('button', {name: 'ExceptionGroup 2: child'}) - ).toBeInTheDocument(); - }); - - it('sorts children according to sort preference', () => { - render( - - ); - - const children = screen.getAllByRole('button'); - - // Order should be oldest to newest, opposite fo the previous test - expect(within(children[0]!).getByText(/ExceptionGroup 2/i)).toBeInTheDocument(); - expect(within(children[1]!).getByText(/TypeError/i)).toBeInTheDocument(); - }); - - it('renders tree with child exception group', () => { - render(); - - const items = screen.getAllByTestId('exception-tree-item'); - expect(items).toHaveLength(3); - - // Should show and link to parent exception group - expect( - within(items[0]!).getByRole('button', {name: 'ExceptionGroup 1: parent'}) - ).toBeInTheDocument(); - // Should have a link to child exception group - expect(within(items[1]!).getByText('ExceptionGroup 2: child')).toBeInTheDocument(); - // Show show and link to child exception - expect( - within(items[2]!).getByRole('button', {name: 'ValueError: test'}) - ).toBeInTheDocument(); - }); - - it('does not render for sub-exception', () => { - const {container} = render( - - ); - - expect(container).toBeEmptyDOMElement(); - }); -}); diff --git a/static/app/components/events/interfaces/crashContent/exception/relatedExceptions.tsx b/static/app/components/events/interfaces/crashContent/exception/relatedExceptions.tsx deleted file mode 100644 index fbbaf4bad3ee..000000000000 --- a/static/app/components/events/interfaces/crashContent/exception/relatedExceptions.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import {Fragment} from 'react'; -import styled from '@emotion/styled'; - -import {Button} from '@sentry/scraps/button'; - -import {t} from 'sentry/locale'; -import type {ExceptionValue} from 'sentry/types/event'; -import type {StackTraceMechanism} from 'sentry/types/stacktrace'; -import {defined} from 'sentry/utils/defined'; - -type ExceptionGroupContextProps = { - allExceptions: ExceptionValue[]; - onExceptionClick: (exceptionId: number) => void; - mechanism?: StackTraceMechanism | null; - newestFirst?: boolean; -}; - -type ExceptionTreeProps = { - childExceptions: ExceptionValue[]; - onExceptionClick: (exceptionId: number) => void; - exception?: ExceptionValue; - parentException?: ExceptionValue; -}; - -type ExceptionLinkProps = { - exception: ExceptionValue; - link: boolean; - onExceptionClick: (exceptionId: number) => void; -}; - -type ExceptionTreeItemProps = { - exception: ExceptionValue; - level: number; - onExceptionClick: (exceptionId: number) => void; - firstChild?: boolean; - link?: boolean; -}; - -function getExceptionName(exception: ExceptionValue) { - if (exception.type) { - return exception.value ? `${exception.type}: ${exception.value}` : exception.type; - } - - return exception.value ?? t('Exception'); -} - -function ExceptionLink({exception, link, onExceptionClick}: ExceptionLinkProps) { - const exceptionName = getExceptionName(exception); - - const exceptionId = exception.mechanism?.exception_id; - - if (!defined(exceptionId) || !link) { - return
{exceptionName}
; - } - - return ( - - ); -} - -function TreeChildLine({firstChild}: {firstChild?: boolean}) { - return ( - - - - - ); -} - -function ExceptionTreeItem({ - exception, - level, - firstChild, - link = true, - onExceptionClick, -}: ExceptionTreeItemProps) { - return ( - - {level > 0 && } - - - - ); -} - -function ExceptionTree({ - parentException, - exception, - childExceptions, - onExceptionClick, -}: ExceptionTreeProps) { - if (!exception) { - return null; - } - - return ( - - {parentException && ( - - )} - - {childExceptions.map((childException, i) => ( - - ))} - - ); -} - -export function RelatedExceptions({ - allExceptions, - mechanism, - newestFirst, - onExceptionClick, -}: ExceptionGroupContextProps) { - if (!mechanism?.is_exception_group) { - return null; - } - - const parentException = allExceptions.find( - exc => exc.mechanism?.exception_id === mechanism.parent_id - ); - const exception = allExceptions.find( - exc => exc.mechanism?.exception_id === mechanism.exception_id - ); - const childExceptions = allExceptions.filter( - exc => exc.mechanism?.parent_id === mechanism.exception_id - ); - - if (newestFirst) { - childExceptions.reverse(); - } - - return ( - - Related Exceptions - - - ); -} - -const Heading = styled('div')` - font-weight: ${p => p.theme.font.weight.sans.medium}; - font-size: ${p => p.theme.font.size.md}; - margin: ${p => p.theme.space.md} 0 ${p => p.theme.space.xs} 0; - color: ${p => p.theme.tokens.content.secondary}; -`; - -const StyledPre = styled('pre')` - margin: 0; - overflow-x: auto; -`; - -const TreeChildLineSvg = styled('svg')` - position: absolute; - left: 6px; - bottom: 50%; -`; - -const TreeItem = styled('div')<{level: number}>` - position: relative; - display: grid; - align-items: center; - grid-template-columns: auto auto 1fr; - gap: ${p => p.theme.space.md}; - padding-left: ${p => (p.level > 0 ? 20 : 0)}px; - margin-left: ${p => Math.max((p.level - 1) * 20, 0)}px; - height: 24px; - white-space: nowrap; -`; - -const Circle = styled('div')` - border-radius: 50%; - height: 12px; - width: 12px; - border: 1px solid ${p => p.theme.tokens.border.primary}; -`; diff --git a/static/app/components/events/interfaces/crashContent/exception/stackTrace.spec.tsx b/static/app/components/events/interfaces/crashContent/exception/stackTrace.spec.tsx deleted file mode 100644 index 7b671190937e..000000000000 --- a/static/app/components/events/interfaces/crashContent/exception/stackTrace.spec.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import {EventFixture} from 'sentry-fixture/event'; - -import {render, screen} from 'sentry-test/reactTestingLibrary'; -import {textWithMarkupMatcher} from 'sentry-test/utils'; - -import {StackTrace as ExceptionStacktraceContent} from 'sentry/components/events/interfaces/crashContent/exception/stackTrace'; -import {StackType, StackView} from 'sentry/types/stacktrace'; - -const frames = [ - { - function: null, - colNo: null, - vars: {}, - symbol: null, - module: '', - lineNo: null, - package: null, - absPath: 'https://sentry.io/hiventy/kraken-prod/issues/438681831/?referrer=slack#', - inApp: false, - instructionAddr: null, - filename: '/hiventy/kraken-prod/issues/438681831/', - platform: null, - context: [], - symbolAddr: null, - rawFunction: null, - trust: null, - }, - { - absPath: 'foo/baz.c', - colNo: null, - context: [], - filename: 'foo/baz.c', - function: null, - inApp: false, - instructionAddr: null, - lineNo: 1, - module: null, - package: null, - platform: null, - rawFunction: null, - symbol: null, - symbolAddr: null, - trust: null, - vars: null, - }, -]; - -const stacktrace: React.ComponentProps['stacktrace'] = - { - framesOmitted: null, - hasSystemFrames: false, - registers: {}, - frames, - }; - -const props: React.ComponentProps = { - platform: 'node', - expandFirstFrame: true, - newestFirst: true, - chainedException: false, - stackType: StackType.ORIGINAL, - event: EventFixture({ - entries: [], - crashFile: { - sha1: 'sha1', - name: 'name.dmp', - dateCreated: '2019-05-21T18:01:48.762Z', - headers: {'Content-Type': 'application/octet-stream'}, - id: '12345', - size: 123456, - type: 'event.minidump', - }, - culprit: '', - dateCreated: '2019-05-21T18:00:23Z', - 'event.type': 'error', - eventID: '123456', - groupID: '1', - id: '98654', - location: 'main.js', - message: 'TestException', - platform: 'native', - projectID: '123', - tags: [{value: 'production', key: 'production'}], - title: 'TestException', - }), - data: stacktrace, - stacktrace, - groupingCurrentLevel: undefined, - meta: undefined, -}; - -describe('ExceptionStacktraceContent', () => { - it('default behaviour', () => { - render(); - }); - - it('should return an emptyRender', () => { - const {container} = render( - - ); - - expect(container).toBeEmptyDOMElement(); - }); - - it('shows stack trace', () => { - render( - - ); - - expect( - screen.getByText('No app only stack trace has been found!') - ).toBeInTheDocument(); - }); - - it('does not show stack trace', () => { - render( - - ); - expect( - screen.getByText(textWithMarkupMatcher('foo/baz.c at line 1')) - ).toBeInTheDocument(); - }); - - it('should render system frames if "stackView: app" and there are no inApp frames and is a chained exceptions', () => { - render( - - ); - - for (const frame of frames) { - expect(screen.getByText(frame.filename)).toBeInTheDocument(); - } - }); - - it('should not render system frames if "stackView: app" and there are inApp frames and is a chained exceptions', () => { - render( - - ); - - // There must be two elements, one being the inApp frame and the other - // the last frame which is non-app frame - expect(screen.getAllByRole('listitem')).toHaveLength(2); - - // inApp === true - expect(screen.getAllByRole('listitem')[1]).toHaveTextContent(frames[0]!.filename); - - // inApp === false - expect(screen.getAllByRole('listitem')[0]).toHaveTextContent(frames[1]!.filename); - }); -}); diff --git a/static/app/components/events/interfaces/crashContent/exception/stackTrace.tsx b/static/app/components/events/interfaces/crashContent/exception/stackTrace.tsx deleted file mode 100644 index be29998c8196..000000000000 --- a/static/app/components/events/interfaces/crashContent/exception/stackTrace.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import {EmptyMessage} from 'sentry/components/emptyMessage'; -import {Content as StackTraceContent} from 'sentry/components/events/interfaces/crashContent/stackTrace/content'; -import {NativeContent} from 'sentry/components/events/interfaces/crashContent/stackTrace/nativeContent'; -import type {FrameSourceMapDebuggerData} from 'sentry/components/events/interfaces/sourceMapsDebuggerModal'; -import {Panel} from 'sentry/components/panels/panel'; -import {IconWarning} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import type {Event, ExceptionValue} from 'sentry/types/event'; -import type {Group} from 'sentry/types/group'; -import type {PlatformKey} from 'sentry/types/platform'; -import {StackType, StackView} from 'sentry/types/stacktrace'; -import {defined} from 'sentry/utils/defined'; -import {isNativePlatform} from 'sentry/utils/platform'; - -type Props = { - chainedException: boolean; - data: ExceptionValue['stacktrace']; - event: Event; - newestFirst: boolean; - platform: PlatformKey; - stackType: StackType; - stacktrace: ExceptionValue['stacktrace']; - expandFirstFrame?: boolean; - frameSourceMapDebuggerData?: FrameSourceMapDebuggerData[]; - groupingCurrentLevel?: Group['metadata']['current_level']; - meta?: Record; - stackView?: StackView; - threadId?: number; -}; - -export function StackTrace({ - stackView, - stacktrace, - chainedException, - platform, - newestFirst, - groupingCurrentLevel, - data, - expandFirstFrame, - event, - meta, - threadId, - frameSourceMapDebuggerData, - stackType, -}: Props) { - if (!defined(stacktrace)) { - return null; - } - - if ( - stackView === StackView.APP && - !(stacktrace.frames ?? []).some(frame => frame.inApp) && - !chainedException - ) { - return ( - - } - title={t('No app only stack trace has been found!')} - /> - - ); - } - - if (!data) { - return null; - } - - const includeSystemFrames = - stackView === StackView.FULL || - (chainedException && data.frames?.every(frame => !frame.inApp)); - /** - * Armin, Markus: - * If all frames are in app, then no frame is in app. - * This normally does not matter for the UI but when chained exceptions - * are used this causes weird behavior where one exception appears to not have a stack trace. - * - * It is easier to fix the UI logic to show a non-empty stack trace for chained exceptions - */ - - if (isNativePlatform(platform)) { - return ( - - ); - } - - return ( - - ); -} diff --git a/static/app/components/events/interfaces/crashContent/stackTrace/content.spec.tsx b/static/app/components/events/interfaces/crashContent/stackTrace/content.spec.tsx deleted file mode 100644 index 92c1bd51c072..000000000000 --- a/static/app/components/events/interfaces/crashContent/stackTrace/content.spec.tsx +++ /dev/null @@ -1,452 +0,0 @@ -import {EventFixture} from 'sentry-fixture/event'; -import {EventEntryStacktraceFixture} from 'sentry-fixture/eventEntryStacktrace'; -import {GitHubIntegrationFixture} from 'sentry-fixture/githubIntegration'; -import {OrganizationFixture} from 'sentry-fixture/organization'; -import {ProjectFixture} from 'sentry-fixture/project'; -import {RepositoryFixture} from 'sentry-fixture/repository'; -import {RepositoryProjectPathConfigFixture} from 'sentry-fixture/repositoryProjectPathConfig'; - -import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; - -import {Content as StackTraceContent} from 'sentry/components/events/interfaces/crashContent/stackTrace/content'; -import {ProjectsStore} from 'sentry/stores/projectsStore'; -import {EventOrGroupType} from 'sentry/types/event'; -import type {StacktraceType} from 'sentry/types/stacktrace'; - -const organization = OrganizationFixture(); -const project = ProjectFixture(); - -const integration = GitHubIntegrationFixture(); -const repo = RepositoryFixture({integrationId: integration.id}); - -const config = RepositoryProjectPathConfigFixture({project, repo, integration}); - -const eventEntryStacktrace = EventEntryStacktraceFixture(); -const event = EventFixture({ - projectID: project.id, - entries: [eventEntryStacktrace], - type: EventOrGroupType.ERROR, -}); - -const data = eventEntryStacktrace.data as Required; - -describe('StackTrace', () => { - const defaultProps = { - platform: 'other' as const, - newestFirst: true, - className: 'no-exception', - includeSystemFrames: true, - } satisfies Partial>; - - beforeEach(() => { - MockApiClient.clearMockResponses(); - - const promptResponse = { - dismissed_ts: undefined, - snoozed_ts: undefined, - }; - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/prompts-activity/`, - body: promptResponse, - }); - MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/stacktrace-link/`, - body: {config, sourceUrl: 'https://something.io', integrations: [integration]}, - }); - ProjectsStore.loadInitialData([project]); - }); - it('renders', () => { - render(); - - // stack trace content - const stackTraceContent = screen.getByTestId('stack-trace-content'); - expect(stackTraceContent).toBeInTheDocument(); - - // frame list - const frames = screen.getByTestId('frames'); - expect(frames.children).toHaveLength(5); - }); - - it('renders the frame in the correct order', () => { - render(); - - // frame - filename - const frameFilenames = screen.getAllByTestId('filename'); - expect(frameFilenames).toHaveLength(5); - expect(frameFilenames[0]).toHaveTextContent('raven/scripts/runner.py'); - expect(frameFilenames[1]).toHaveTextContent('raven/scripts/runner.py'); - expect(frameFilenames[2]).toHaveTextContent('raven/base.py'); - expect(frameFilenames[3]).toHaveTextContent('raven/base.py'); - expect(frameFilenames[4]).toHaveTextContent('raven/base.py'); - - // frame - function - const frameFunction = screen.getAllByTestId('function'); - expect(frameFunction).toHaveLength(5); - expect(frameFunction[0]).toHaveTextContent('main'); - expect(frameFunction[1]).toHaveTextContent('send_test_message'); - expect(frameFunction[2]).toHaveTextContent('captureMessage'); - expect(frameFunction[3]).toHaveTextContent('capture'); - expect(frameFunction[4]).toHaveTextContent('build_msg'); - }); - - it('collapse/expand frames by clicking anywhere in the frame element', async () => { - render(); - - // frame list - const frames = screen.getByTestId('frames'); - expect(frames.children).toHaveLength(5); - - // only one frame is expanded by default - expect(screen.getByTestId('toggle-button-expanded')).toBeInTheDocument(); - expect(screen.getAllByTestId('toggle-button-collapsed')).toHaveLength(4); - - // clickable list item element - const frameTitles = screen.getAllByTestId('title'); - - // collapse the expanded frame (by default) - await userEvent.click(frameTitles[0]!); - - // all frames are now collapsed - expect(screen.queryByTestId('toggle-button-expanded')).not.toBeInTheDocument(); - expect(screen.getAllByTestId('toggle-button-collapsed')).toHaveLength(5); - - // expand penultimate and last frame - await userEvent.click(frameTitles[frameTitles.length - 2]!); - await userEvent.click(frameTitles[frameTitles.length - 1]!); - - // two frames are now collapsed - expect(screen.getAllByTestId('toggle-button-expanded')).toHaveLength(2); - expect(screen.getAllByTestId('toggle-button-collapsed')).toHaveLength(3); - }); - - it('collapse/expand frames by clicking on the toggle button', async () => { - render(); - - // frame list - const frames = screen.getByTestId('frames'); - expect(frames.children).toHaveLength(5); - - const expandedToggleButtons = screen.getByTestId('toggle-button-expanded'); - - // only one frame is expanded by default - expect(expandedToggleButtons).toBeInTheDocument(); - expect(screen.getAllByTestId('toggle-button-collapsed')).toHaveLength(4); - - // collapse the expanded frame (by default) - await userEvent.click(expandedToggleButtons); - - // all frames are now collapsed - expect(screen.queryByTestId('toggle-button-expanded')).not.toBeInTheDocument(); - expect(screen.getAllByTestId('toggle-button-collapsed')).toHaveLength(5); - - const collapsedToggleButtons = screen.getAllByTestId('toggle-button-collapsed'); - - // expand penultimate and last frame - await userEvent.click(collapsedToggleButtons[collapsedToggleButtons.length - 2]!); - await userEvent.click(collapsedToggleButtons[collapsedToggleButtons.length - 1]!); - - // two frames are now collapsed - expect(screen.getAllByTestId('toggle-button-expanded')).toHaveLength(2); - expect(screen.getAllByTestId('toggle-button-collapsed')).toHaveLength(3); - }); - - it('if all in_app equals false, all the frames are showing by default', () => { - render(); - - // frame list - const frames = screen.getByTestId('frames'); - expect(frames.children).toHaveLength(5); - }); - - it('if frames are omitted, renders omitted frames', () => { - const newData: StacktraceType = { - ...data, - framesOmitted: [0, 3], - }; - - render(); - - const omittedFrames = screen.getByText( - 'Frames 0 to 3 were omitted and not available.' - ); - expect(omittedFrames).toBeInTheDocument(); - }); - - it('does not render non in app tags', () => { - const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0]!, inApp: false}; - - const newData = { - ...data, - frames: dataFrames, - }; - - render(); - - expect(screen.queryByText('System')).not.toBeInTheDocument(); - }); - - it('displays a toggle button when there is more than one non-inapp frame', () => { - const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0]!, inApp: true}; - - const newData = { - ...data, - frames: dataFrames, - }; - - render( - - ); - - expect(screen.getByText('Show 3 more frames')).toBeInTheDocument(); - }); - - it('shows/hides frames when toggle button clicked', async () => { - const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0]!, inApp: true}; - dataFrames[1] = {...dataFrames[1]!, function: 'non-in-app-frame'}; - dataFrames[2] = {...dataFrames[2]!, function: 'non-in-app-frame'}; - dataFrames[3] = {...dataFrames[3]!, function: 'non-in-app-frame'}; - dataFrames[4] = {...dataFrames[4]!, function: 'non-in-app-frame'}; - - const newData = { - ...data, - frames: dataFrames, - }; - - render( - - ); - - await userEvent.click(screen.getByText('Show 3 more frames')); - expect(screen.getAllByText('non-in-app-frame')).toHaveLength(4); - await userEvent.click(screen.getByText('Hide 3 more frames')); - expect(screen.getByText('non-in-app-frame')).toBeInTheDocument(); - }); - - it('does not display a toggle button when there is only one non-inapp frame', () => { - const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0]!, inApp: true}; - dataFrames[2] = {...dataFrames[2]!, inApp: true}; - dataFrames[4] = {...dataFrames[4]!, inApp: true}; - - const newData = { - ...data, - frames: dataFrames, - }; - - render( - - ); - - expect(screen.queryByText(/Show .* more frames*/)).not.toBeInTheDocument(); - }); - - describe('if there is a frame with in_app equal to true, display only in_app frames', () => { - it('displays crashed from only', () => { - const dataFrames = [...data.frames]; - - const newData = { - ...data, - hasSystemFrames: true, - frames: [ - {...dataFrames[0]!, inApp: true}, - ...dataFrames.splice(1, dataFrames.length), - ], - }; - - render( - - ); - - // clickable list item element - const frameTitles = screen.getAllByTestId('title'); - - // frame list - in app only - expect(frameTitles).toHaveLength(2); - - expect(frameTitles[0]).toHaveTextContent( - 'Crashed in non-app: raven/scripts/runner.py in main at line 112' - ); - expect(frameTitles[1]).toHaveTextContent('raven/base.py in build_msg at line 303'); - }); - - it('displays called from only', () => { - const dataFrames = [...data.frames]; - - const newData = { - ...data, - hasSystemFrames: true, - registers: {}, - frames: [ - ...dataFrames.splice(0, dataFrames.length - 1), - {...dataFrames[dataFrames.length - 1]!, inApp: true}, - ], - }; - - render( - - ); - - // clickable list item element - const frameTitles = screen.getAllByTestId('title'); - - // frame list - in app only - expect(frameTitles).toHaveLength(2); - - expect(frameTitles[0]).toHaveTextContent( - 'raven/scripts/runner.py in main at line 112' - ); - expect(frameTitles[1]).toHaveTextContent( - 'Called from: raven/scripts/runner.py in send_test_message at line 77' - ); - }); - - it('displays crashed from and called from', () => { - const dataFrames = [...data.frames]; - - const newData = { - ...data, - hasSystemFrames: true, - frames: [ - ...dataFrames.slice(0, 1), - {...dataFrames[1]!, inApp: true}, - ...dataFrames.slice(2, dataFrames.length), - ], - }; - - render( - - ); - - // clickable list item element - const frameTitles = screen.getAllByTestId('title'); - - // frame list - in app only - expect(frameTitles).toHaveLength(3); - - expect(frameTitles[0]).toHaveTextContent( - 'Crashed in non-app: raven/scripts/runner.py in main at line 112' - ); - expect(frameTitles[1]).toHaveTextContent('raven/base.py in capture at line 459'); - expect(frameTitles[2]).toHaveTextContent( - 'Called from: raven/base.py in build_msg at line 303' - ); - }); - - it('displays "occurred in" when event is not an error', () => { - const dataFrames = [...data.frames]; - - const newData = { - ...data, - hasSystemFrames: true, - frames: [ - {...dataFrames[0]!, inApp: true}, - ...dataFrames.splice(1, dataFrames.length), - ], - }; - - render( - - ); - - // clickable list item element - const frameTitles = screen.getAllByTestId('title'); - - // frame list - in app only - expect(frameTitles).toHaveLength(2); - - expect(frameTitles[0]).toHaveTextContent( - 'Occurred in non-app: raven/scripts/runner.py in main at line 112' - ); - expect(frameTitles[1]).toHaveTextContent('raven/base.py in build_msg at line 303'); - }); - - it('displays "occurred in" when event is an ANR error', () => { - const dataFrames = [...data.frames]; - - const newData = { - ...data, - hasSystemFrames: true, - frames: [ - {...dataFrames[0]!, inApp: true}, - ...dataFrames.splice(1, dataFrames.length), - ], - }; - - render( - - ); - - // clickable list item element - const frameTitles = screen.getAllByTestId('title'); - - // frame list - in app only - expect(frameTitles).toHaveLength(2); - - expect(frameTitles[0]).toHaveTextContent( - 'Occurred in non-app: raven/scripts/runner.py in main at line 112' - ); - expect(frameTitles[1]).toHaveTextContent('raven/base.py in build_msg at line 303'); - }); - }); -}); diff --git a/static/app/components/events/interfaces/crashContent/stackTrace/content.tsx b/static/app/components/events/interfaces/crashContent/stackTrace/content.tsx deleted file mode 100644 index 5646506de830..000000000000 --- a/static/app/components/events/interfaces/crashContent/stackTrace/content.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import {Fragment, useState} from 'react'; -import styled from '@emotion/styled'; - -import { - DeprecatedLine, - type DeprecatedLineProps, -} from 'sentry/components/events/interfaces/frame/deprecatedLine'; -import type {FrameSourceMapDebuggerData} from 'sentry/components/events/interfaces/sourceMapsDebuggerModal'; -import { - getHiddenFrameIndices, - getLastFrameIndex, - isRepeatedFrame, -} from 'sentry/components/events/interfaces/utils'; -import {Panel} from 'sentry/components/panels/panel'; -import type {Event, Frame} from 'sentry/types/event'; -import type {PlatformKey} from 'sentry/types/platform'; -import type {StackTraceMechanism, StacktraceType} from 'sentry/types/stacktrace'; - -import {OmittedFrames} from './omittedFrames'; - -type DefaultProps = { - expandFirstFrame: boolean; - includeSystemFrames: boolean; -}; - -type Props = { - data: StacktraceType; - event: Event; - newestFirst: boolean; - platform: PlatformKey; - className?: string; - frameSourceMapDebuggerData?: FrameSourceMapDebuggerData[]; - hideSourceMapDebugger?: boolean; - isHoverPreviewed?: boolean; - lockAddress?: string; - mechanism?: StackTraceMechanism | null; - meta?: Record; - threadId?: number; -} & Partial; - -export function Content({ - data, - event, - className, - newestFirst, - expandFirstFrame = true, - platform, - includeSystemFrames = true, - isHoverPreviewed = false, - meta, - threadId, - lockAddress, - frameSourceMapDebuggerData, - hideSourceMapDebugger = false, -}: Props) { - const [toggleFrameMap, setToggleFrameMap] = useState(setInitialFrameMap()); - - const {frames = [], registers} = data; - - function frameIsVisible(frame: Frame, nextFrame: Frame) { - return ( - includeSystemFrames || - frame.inApp || - nextFrame?.inApp || - // the last non-app frame - (!frame.inApp && !nextFrame) - ); - } - - function setInitialFrameMap(): Record { - const indexMap: Record = {}; - (data.frames ?? []).forEach((frame, frameIdx) => { - const nextFrame = (data.frames ?? [])[frameIdx + 1]!; - const repeatedFrame = isRepeatedFrame(frame, nextFrame); - if (frameIsVisible(frame, nextFrame) && !repeatedFrame && !frame.inApp) { - indexMap[frameIdx] = false; - } - }); - return indexMap; - } - - function getInitialFrameCounts(): Record { - let count = 0; - const countMap: Record = {}; - (data.frames ?? []).forEach((frame, frameIdx) => { - const nextFrame = (data.frames ?? [])[frameIdx + 1]!; - const repeatedFrame = isRepeatedFrame(frame, nextFrame); - if (frameIsVisible(frame, nextFrame) && !repeatedFrame && !frame.inApp) { - countMap[frameIdx] = count; - count = 0; - } else { - if (!repeatedFrame && !frame.inApp) { - count += 1; - } - } - }); - return countMap; - } - - const handleToggleFrames = ( - mouseEvent: React.MouseEvent, - frameIndex: number - ) => { - mouseEvent.stopPropagation(); // to prevent toggling frame context - - setToggleFrameMap(prevState => ({ - ...prevState, - [frameIndex]: !prevState[frameIndex], - })); - }; - - const lastFrameIndex = getLastFrameIndex(frames); - const frameCountMap = getInitialFrameCounts(); - const hiddenFrameIndices = getHiddenFrameIndices({ - data, - toggleFrameMap, - frameCountMap, - }); - - const mechanism = - platform === 'java' && event.tags?.find(({key}) => key === 'mechanism')?.value; - const isANR = mechanism === 'ANR' || mechanism === 'AppExitInfo'; - - let nRepeats = 0; - - const convertedFrames = frames - .map((frame, frameIndex) => { - const nextFrame = frames[frameIndex + 1]!; - const repeatedFrame = isRepeatedFrame(frame, nextFrame); - const isLastFrame = frameIndex === frames.length - 1; - - if (repeatedFrame) { - nRepeats++; - } - - if ( - (frameIsVisible(frame, nextFrame) && !repeatedFrame) || - hiddenFrameIndices.includes(frameIndex) - ) { - const frameProps: DeprecatedLineProps = { - event, - data: frame, - isExpanded: expandFirstFrame && lastFrameIndex === frameIndex, - emptySourceNotation: lastFrameIndex === frameIndex && frameIndex === 0, - nextFrame, - platform, - timesRepeated: nRepeats, - registers: isLastFrame ? registers : {}, - onShowFramesToggle: (e: React.MouseEvent) => { - handleToggleFrames(e, frameIndex); - }, - isSubFrame: hiddenFrameIndices.includes(frameIndex), - isShowFramesToggleExpanded: toggleFrameMap[frameIndex], - isHoverPreviewed, - frameMeta: meta?.frames?.[frameIndex], - registersMeta: meta?.registers, - isANR, - threadId, - lockAddress, - hiddenFrameCount: frameCountMap[frameIndex], - frameSourceResolutionResults: frameSourceMapDebuggerData?.[frameIndex], - hideSourceMapDebugger, - }; - - nRepeats = 0; - - if (frameIndex === data.framesOmitted?.[0]) { - return ( - - - - - ); - } - - return ; - } - - if (!repeatedFrame) { - nRepeats = 0; - } - - if (frameIndex !== data.framesOmitted?.[0]) { - return null; - } - - return ; - }) - .filter((frame): frame is React.ReactElement => !!frame); - - const wrapperClassName = `${!!className && className} traceback ${ - includeSystemFrames ? 'full-traceback' : 'in-app-traceback' - }`; - - return ( - - - - {newestFirst ? convertedFrames.toReversed() : convertedFrames} - - - - ); -} - -const Wrapper = styled('div')` - position: relative; -`; - -const StackTraceContentPanel = styled(Panel)` - position: relative; - overflow: hidden; -`; - -const StyledList = styled('ul')` - list-style: none; -`; diff --git a/static/app/components/events/interfaces/crashContent/stackTrace/index.tsx b/static/app/components/events/interfaces/crashContent/stackTrace/index.tsx deleted file mode 100644 index 5355ca1d224a..000000000000 --- a/static/app/components/events/interfaces/crashContent/stackTrace/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import {ErrorBoundary} from 'sentry/components/errorBoundary'; -import type {Event} from 'sentry/types/event'; -import type {PlatformKey} from 'sentry/types/platform'; -import type {StacktraceType} from 'sentry/types/stacktrace'; -import {StackView} from 'sentry/types/stacktrace'; -import {isNativePlatform} from 'sentry/utils/platform'; - -import {Content} from './content'; -import {NativeContent} from './nativeContent'; -import {displayRawContent as rawStacktraceContent} from './rawContent'; - -type Props = { - event: Event; - newestFirst: boolean; - platform: PlatformKey; - stackView: StackView; - stacktrace: StacktraceType; - groupingCurrentLevel?: number; - lockAddress?: string; - meta?: Record; - threadId?: number; -}; - -export function StackTraceContent({ - stackView, - stacktrace, - event, - newestFirst, - platform, - groupingCurrentLevel, - meta, - threadId, - lockAddress, -}: Props) { - if (stackView === StackView.RAW) { - return ( - -
-          {rawStacktraceContent({data: stacktrace, platform: event.platform})}
-        
-
- ); - } - - if (isNativePlatform(platform)) { - return ( - - - - ); - } - - return ( - - - - ); -} diff --git a/static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.spec.tsx b/static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.spec.tsx deleted file mode 100644 index 3438acf863de..000000000000 --- a/static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.spec.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import {EventFixture} from 'sentry-fixture/event'; -import {EventEntryStacktraceFixture} from 'sentry-fixture/eventEntryStacktrace'; -import {EventStacktraceFrameFixture} from 'sentry-fixture/eventStacktraceFrame'; -import {GitHubIntegrationFixture} from 'sentry-fixture/githubIntegration'; -import {OrganizationFixture} from 'sentry-fixture/organization'; -import {ProjectFixture} from 'sentry-fixture/project'; -import {RepositoryFixture} from 'sentry-fixture/repository'; -import {RepositoryProjectPathConfigFixture} from 'sentry-fixture/repositoryProjectPathConfig'; - -import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; - -import {Content as StackTraceContent} from 'sentry/components/events/interfaces/crashContent/stackTrace/content'; -import {NativeContent} from 'sentry/components/events/interfaces/crashContent/stackTrace/nativeContent'; -import {SymbolicatorStatus} from 'sentry/components/events/interfaces/types'; -import {ProjectsStore} from 'sentry/stores/projectsStore'; -import {EventOrGroupType} from 'sentry/types/event'; -import type {StacktraceType} from 'sentry/types/stacktrace'; - -const organization = OrganizationFixture(); -const project = ProjectFixture(); - -const integration = GitHubIntegrationFixture(); -const repo = RepositoryFixture({integrationId: integration.id}); - -const config = RepositoryProjectPathConfigFixture({project, repo, integration}); - -const eventEntryStacktrace = EventEntryStacktraceFixture(); -const event = EventFixture({ - projectID: project.id, - entries: [eventEntryStacktrace], - type: EventOrGroupType.ERROR, -}); - -const data = eventEntryStacktrace.data as Required; - -function renderedComponent( - props: Partial> -) { - return render( - - ); -} -describe('Native StackTrace', () => { - beforeEach(() => { - MockApiClient.clearMockResponses(); - const promptResponse = { - dismissed_ts: undefined, - snoozed_ts: undefined, - }; - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/prompts-activity/`, - body: promptResponse, - }); - MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/stacktrace-link/`, - body: {config, sourceUrl: 'https://something.io', integrations: [integration]}, - }); - ProjectsStore.loadInitialData([project]); - }); - it('does not render non in app tags', () => { - const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0]!, inApp: false}; - - const newData = { - ...data, - frames: dataFrames, - }; - - renderedComponent({ - data: newData, - }); - - expect(screen.queryByText('System')).not.toBeInTheDocument(); - }); - - it('displays a toggle button when there is more than one non-inapp frame', () => { - const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0]!, inApp: true}; - - const newData = { - ...data, - frames: dataFrames, - }; - - renderedComponent({ - data: newData, - includeSystemFrames: false, - }); - - expect(screen.getByText('Show 3 more frames')).toBeInTheDocument(); - }); - - it('shows/hides frames when toggle button clicked', async () => { - const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0]!, inApp: true}; - dataFrames[1] = {...dataFrames[1]!, function: 'non-in-app-frame'}; - dataFrames[2] = {...dataFrames[2]!, function: 'non-in-app-frame'}; - dataFrames[3] = {...dataFrames[3]!, function: 'non-in-app-frame'}; - dataFrames[4] = {...dataFrames[4]!, function: 'non-in-app-frame'}; - - const newData = { - ...data, - frames: dataFrames, - }; - - renderedComponent({ - data: newData, - includeSystemFrames: false, - }); - await userEvent.click(screen.getByText('Show 3 more frames')); - expect(screen.getAllByText('non-in-app-frame')).toHaveLength(4); - await userEvent.click(screen.getByText('Hide 3 more frames')); - expect(screen.getByText('non-in-app-frame')).toBeInTheDocument(); - }); - - it('does not display a toggle button when there is only one non-inapp frame', () => { - const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0]!, inApp: true}; - dataFrames[2] = {...dataFrames[2]!, inApp: true}; - dataFrames[4] = {...dataFrames[4]!, inApp: true}; - - const newData = { - ...data, - frames: dataFrames, - }; - - renderedComponent({ - data: newData, - includeSystemFrames: false, - }); - - expect(screen.queryByText(/Show .* more frames*/)).not.toBeInTheDocument(); - }); - - it('displays correct icons from frame symbolicatorStatus when image does not exist', () => { - const newData = { - ...data, - frames: [ - EventStacktraceFrameFixture({ - symbolicatorStatus: SymbolicatorStatus.MISSING, - function: 'missing()', - }), - EventStacktraceFrameFixture({ - symbolicatorStatus: SymbolicatorStatus.MISSING_SYMBOL, - function: 'missing_symbol()', - }), - EventStacktraceFrameFixture({ - symbolicatorStatus: SymbolicatorStatus.SYMBOLICATED, - function: 'symbolicated()', - }), - ], - }; - - render( - - ); - - const frames = screen.getAllByTestId('stack-trace-frame'); - - expect( - within(frames[0]!).getByTestId('symbolication-error-icon') - ).toBeInTheDocument(); - expect( - within(frames[1]!).getByTestId('symbolication-warning-icon') - ).toBeInTheDocument(); - expect(within(frames[2]!).queryByTestId(/symbolication/)).not.toBeInTheDocument(); - }); - - it('expands the first in app frame', () => { - const newData = { - ...data, - frames: [ - EventStacktraceFrameFixture({ - symbolicatorStatus: SymbolicatorStatus.MISSING, - function: 'missing()', - inApp: true, - }), - EventStacktraceFrameFixture({ - symbolicatorStatus: SymbolicatorStatus.UNKNOWN_IMAGE, - function: 'unknown_image()', - inApp: false, - }), - EventStacktraceFrameFixture({ - symbolicatorStatus: SymbolicatorStatus.SYMBOLICATED, - function: 'symbolicated()', - inApp: true, - }), - ], - }; - - render(); - - expect(screen.getByRole('button', {name: 'Collapse Context'})).toBeInTheDocument(); - const collapsed = screen.getAllByRole('button', {name: 'Expand Context'}); - expect(collapsed).toHaveLength(2); - }); -}); diff --git a/static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.tsx b/static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.tsx deleted file mode 100644 index 36c32457d2c6..000000000000 --- a/static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import {Fragment, useCallback, useState} from 'react'; -import styled from '@emotion/styled'; - -import {NativeFrame} from 'sentry/components/events/interfaces/nativeFrame'; -import { - findImageForAddress, - getHiddenFrameIndices, - getLastFrameIndex, - isRepeatedFrame, - parseAddress, -} from 'sentry/components/events/interfaces/utils'; -import {Panel} from 'sentry/components/panels/panel'; -import type {Event, Frame} from 'sentry/types/event'; -import type {Group} from 'sentry/types/group'; -import type {PlatformKey} from 'sentry/types/platform'; -import type {StacktraceType} from 'sentry/types/stacktrace'; - -import {OmittedFrames} from './omittedFrames'; - -function isFrameUsedForGrouping( - frame: Frame, - groupingCurrentLevel: Group['metadata']['current_level'] -): boolean { - const {minGroupingLevel} = frame; - - if (groupingCurrentLevel === undefined || minGroupingLevel === undefined) { - return false; - } - - return minGroupingLevel <= groupingCurrentLevel; -} - -type Props = { - data: StacktraceType; - event: Event; - newestFirst: boolean; - platform: PlatformKey; - className?: string; - groupingCurrentLevel?: Group['metadata']['current_level']; - includeSystemFrames?: boolean; - isHoverPreviewed?: boolean; - meta?: Record; -}; - -export function NativeContent({ - className, - data, - platform, - event, - newestFirst, - isHoverPreviewed, - groupingCurrentLevel, - includeSystemFrames = true, - meta, -}: Props) { - const frames = data.frames ?? []; - - const isDartAsyncSuspensionFrame = useCallback((frame: Frame): boolean => { - return ( - frame.filename === '' || - frame.absPath === '' - ); - }, []); - - const frameIsVisible = useCallback( - (frame: Frame, nextFrame: Frame) => { - if (!includeSystemFrames && isDartAsyncSuspensionFrame(frame)) { - return false; - } - - return ( - includeSystemFrames || - frame.inApp || - nextFrame?.inApp || - // the last non-app frame - (!frame.inApp && !nextFrame) || - isFrameUsedForGrouping(frame, groupingCurrentLevel) - ); - }, - [includeSystemFrames, groupingCurrentLevel, isDartAsyncSuspensionFrame] - ); - - function setInitialFrameMap(): Record { - const indexMap: Record = {}; - frames.forEach((frame, frameIdx) => { - const nextFrame = frames[frameIdx + 1]!; - const repeatedFrame = isRepeatedFrame(frame, nextFrame); - if (frameIsVisible(frame, nextFrame) && !repeatedFrame && !frame.inApp) { - indexMap[frameIdx] = false; - } - }); - return indexMap; - } - - const [toggleFrameMap, setToggleFrameMap] = useState(() => setInitialFrameMap()); - - function getInitialFrameCounts(): Record { - let count = 0; - const countMap: Record = {}; - frames.forEach((frame, frameIdx) => { - const nextFrame = (data.frames ?? [])[frameIdx + 1]!; - const repeatedFrame = isRepeatedFrame(frame, nextFrame); - if (frameIsVisible(frame, nextFrame) && !repeatedFrame && !frame.inApp) { - countMap[frameIdx] = count; - count = 0; - } else { - if (!repeatedFrame && !frame.inApp) { - count += 1; - } - } - }); - return countMap; - } - - const handleToggleFrames = ( - mouseEvent: React.MouseEvent, - frameIndex: number - ) => { - mouseEvent.stopPropagation(); // to prevent toggling frame context - - setToggleFrameMap(prevState => ({ - ...prevState, - [frameIndex]: !prevState[frameIndex], - })); - }; - - const lastFrameIndex = getLastFrameIndex(frames); - const frameCountMap = getInitialFrameCounts(); - const hiddenFrameIndices = getHiddenFrameIndices({ - data, - toggleFrameMap, - frameCountMap, - }); - - const maxLengthOfAllRelativeAddresses = frames.reduce( - (maxLengthUntilThisPoint, frame) => { - const correspondingImage = findImageForAddress({ - event, - addrMode: frame.addrMode, - address: frame.instructionAddr, - }); - - if (!frame.instructionAddr || !correspondingImage?.image_addr) { - return maxLengthUntilThisPoint; - } - - const relativeAddress = ( - parseAddress(frame.instructionAddr) - parseAddress(correspondingImage.image_addr) - ).toString(16); - - return maxLengthUntilThisPoint > relativeAddress.length - ? maxLengthUntilThisPoint - : relativeAddress.length; - }, - 0 - ); - - const firstInAppFrameIndex = frames[newestFirst ? 'findLastIndex' : 'findIndex']( - frame => frame.inApp - ); - let convertedFrames = frames - .map((frame, frameIndex) => { - const prevFrame = frames[frameIndex - 1]; - const nextFrame = frames[frameIndex + 1]!; - const repeatedFrame = isRepeatedFrame(frame, nextFrame); - const isLastFrame = frameIndex === frames.length - 1; - - if ( - (frameIsVisible(frame, nextFrame) && !repeatedFrame) || - hiddenFrameIndices.includes(frameIndex) - ) { - const frameProps: React.ComponentProps = { - event, - frame, - prevFrame, - nextFrame, - emptySourceNotation: lastFrameIndex === frameIndex && frameIndex === 0, - platform, - onShowFramesToggle: (e: React.MouseEvent) => { - handleToggleFrames(e, frameIndex); - }, - image: findImageForAddress({ - event, - addrMode: frame.addrMode, - address: frame.instructionAddr, - }), - maxLengthOfRelativeAddress: maxLengthOfAllRelativeAddresses, - registers: isLastFrame ? data.registers : null, - hiddenFrameCount: frameCountMap[frameIndex], - isHoverPreviewed, - isShowFramesToggleExpanded: toggleFrameMap[frameIndex] ?? false, - isSubFrame: hiddenFrameIndices.includes(frameIndex), - isFirstInAppFrame: firstInAppFrameIndex === frameIndex, - isUsedForGrouping: isFrameUsedForGrouping(frame, groupingCurrentLevel), - frameMeta: meta?.frames?.[frameIndex], - registersMeta: meta?.registers, - }; - - if (frameIndex === data.framesOmitted?.[0]) { - return ( - - - - - ); - } - - return ; - } - - if (frameIndex !== data.framesOmitted?.[0]) { - return null; - } - - return ; - }) - .filter((frame): frame is React.ReactElement => !!frame); - - if (newestFirst) { - convertedFrames = convertedFrames.toReversed(); - } - - const wrapperClassName = `traceback ${ - includeSystemFrames ? 'full-traceback' : 'in-app-traceback' - } ${className}`; - - return ( - - - {convertedFrames} - - - ); -} - -const Wrapper = styled('div')` - position: relative; -`; - -const ContentPanel = styled(Panel)` - position: relative; - overflow: hidden; -`; - -const Frames = styled('ul')` - list-style: none; -`; diff --git a/static/app/components/events/interfaces/crashContent/stackTrace/omittedFrames.tsx b/static/app/components/events/interfaces/crashContent/stackTrace/omittedFrames.tsx deleted file mode 100644 index 00548b002efe..000000000000 --- a/static/app/components/events/interfaces/crashContent/stackTrace/omittedFrames.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import styled from '@emotion/styled'; - -import {t} from 'sentry/locale'; -import type {StacktraceType} from 'sentry/types/stacktrace'; - -export function OmittedFrames({ - omittedFrames, -}: { - omittedFrames: StacktraceType['framesOmitted']; -}) { - if (!omittedFrames) { - return null; - } - - const [start, end] = omittedFrames; - return ( - - {t('Frames %d to %d were omitted and not available.', start, end)} - - ); -} - -const FramesOmittedListItem = styled('li')` - color: #493e54; - font-size: 14px; - font-weight: ${p => p.theme.font.weight.sans.medium}; - border-left: 2px solid ${p => p.theme.colors.red400}; - border-top: 1px solid ${p => p.theme.tokens.border.primary}; - background: ${p => p.theme.colors.red100}; - padding: ${p => p.theme.space.md} ${p => p.theme.space.xl}; -`; diff --git a/static/app/components/events/interfaces/exception.tsx b/static/app/components/events/interfaces/exception.tsx deleted file mode 100644 index f0b90249fd76..000000000000 --- a/static/app/components/events/interfaces/exception.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import {Fragment} from 'react'; - -import {ErrorBoundary} from 'sentry/components/errorBoundary'; -import {StacktraceContext} from 'sentry/components/events/interfaces/stackTraceContext'; -import {SuspectCommits} from 'sentry/components/events/suspectCommits'; -import {TraceEventDataSection} from 'sentry/components/events/traceEventDataSection'; -import {t} from 'sentry/locale'; -import type {Event, ExceptionType} from 'sentry/types/event'; -import {EntryType} from 'sentry/types/event'; -import type {Group} from 'sentry/types/group'; -import type {Project} from 'sentry/types/project'; -import {SectionDivider} from 'sentry/views/issueDetails/foldSection'; - -import {ExceptionContent} from './crashContent/exception'; -import {NoStackTraceMessage} from './noStackTraceMessage'; -import {isStacktraceNewestFirst} from './utils'; - -type Props = { - data: ExceptionType; - event: Event; - group: Group | undefined; - projectSlug: Project['slug']; - groupingCurrentLevel?: Group['metadata']['current_level']; -}; - -export function Exception({ - event, - data, - projectSlug, - group, - groupingCurrentLevel, -}: Props) { - const eventHasThreads = !!event.entries.some(entry => entry.type === EntryType.THREADS); - // in case there are threads in the event data, we don't render the - // exception block. Instead the exception is contained within the - // thread interface. - if (eventHasThreads) { - return null; - } - - const entryIndex = event.entries.findIndex( - eventEntry => eventEntry.type === EntryType.EXCEPTION - ); - - const meta = event._meta?.entries?.[entryIndex]?.data?.values; - - const stackTraceNotFound = !(data.values ?? []).length; - - const hasNonAppFrames = !!data.values?.some(value => - value.stacktrace?.frames?.some(frame => !frame.inApp) - ); - - return ( - - value.rawStacktrace)} - hasVerboseFunctionNames={ - !!data.values?.some( - value => - !!value.stacktrace?.frames?.some( - frame => - !!frame.rawFunction && - !!frame.function && - frame.rawFunction !== frame.function - ) - ) - } - hasAbsoluteFilePaths={ - !!data.values?.some( - value => !!value.stacktrace?.frames?.some(frame => !!frame.filename) - ) - } - hasAbsoluteAddresses={ - !!data.values?.some( - value => !!value.stacktrace?.frames?.some(frame => !!frame.instructionAddr) - ) - } - hasNewestFirst={ - !!data.values?.some(value => (value.stacktrace?.frames ?? []).length > 1) - } - stackTraceNotFound={stackTraceNotFound} - > - {stackTraceNotFound ? ( - - ) : ( - - - {group && ( - - {data.values && data.values.length > 1 && ( - - )} - - - - - )} - - )} - - - ); -} diff --git a/static/app/components/events/interfaces/frame/context.spec.tsx b/static/app/components/events/interfaces/frame/context.spec.tsx deleted file mode 100644 index 609c05b92092..000000000000 --- a/static/app/components/events/interfaces/frame/context.spec.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import {EventFixture} from 'sentry-fixture/event'; -import {OrganizationFixture} from 'sentry-fixture/organization'; -import {ProjectFixture} from 'sentry-fixture/project'; - -import {render, screen} from 'sentry-test/reactTestingLibrary'; - -import {ProjectsStore} from 'sentry/stores/projectsStore'; -import type {Frame} from 'sentry/types/event'; - -import {Context} from './context'; - -describe('Frame - Context', () => { - const org = OrganizationFixture(); - const project = ProjectFixture(); - const event = EventFixture({projectID: project.id}); - const frame = {filename: '/sentry/app.py', lineNo: 233} as Frame; - - beforeEach(() => { - MockApiClient.clearMockResponses(); - ProjectsStore.loadInitialData([project]); - }); - - describe('syntax highlighting', () => { - it('renders code correctly when context lines end in newline characters', () => { - const testFrame: Frame = { - ...frame, - lineNo: 2, - context: [ - [1, 'this is line 1\n'], - [2, 'this is line 2\n'], - [3, 'this is line 3\n'], - ], - }; - - render( - , - {organization: org} - ); - - expect(screen.getAllByTestId('context-line')).toHaveLength(3); - - expect(screen.getByText('this is line 1')).toBeInTheDocument(); - expect(screen.getByText('this is line 2')).toBeInTheDocument(); - expect(screen.getByText('this is line 3')).toBeInTheDocument(); - }); - }); -}); diff --git a/static/app/components/events/interfaces/frame/context.tsx b/static/app/components/events/interfaces/frame/context.tsx deleted file mode 100644 index d57396b3d247..000000000000 --- a/static/app/components/events/interfaces/frame/context.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import {Fragment, useMemo} from 'react'; -import styled from '@emotion/styled'; - -import {ClippedBox} from 'sentry/components/clippedBox'; -import {parseAssembly} from 'sentry/components/events/interfaces/utils'; -import {LoadingIndicator} from 'sentry/components/loadingIndicator'; -import {IconFlag} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import type {Event, Frame} from 'sentry/types/event'; -import type { - SentryAppComponent, - SentryAppSchemaStacktraceLink, -} from 'sentry/types/integrations'; -import type {PlatformKey} from 'sentry/types/platform'; -import type {StacktraceType} from 'sentry/types/stacktrace'; -import {defined} from 'sentry/utils/defined'; -import {getFileExtension} from 'sentry/utils/fileExtension'; -import {useOrganization} from 'sentry/utils/useOrganization'; -import {useProjects} from 'sentry/utils/useProjects'; - -import {Assembly} from './assembly'; -import {ContextLineNumber} from './contextLineNumber'; -import {FrameRegisters} from './frameRegisters'; -import {FrameVariables} from './frameVariables'; -import {usePrismTokensSourceContext} from './usePrismTokensSourceContext'; -import {useSourceContext} from './useSourceContext'; -import {hasPotentialSourceContext} from './utils'; - -type Props = { - components: Array>; - event: Event; - frame: Frame; - registers: StacktraceType['registers']; - className?: string; - emptySourceNotation?: boolean; - frameMeta?: Record; - hasAssembly?: boolean; - hasContextRegisters?: boolean; - hasContextSource?: boolean; - hasContextVars?: boolean; - hasScmSourceContext?: boolean; - isExpanded?: boolean; - platform?: PlatformKey; - registersMeta?: Record; -}; - -export function Context({ - hasContextVars = false, - hasContextSource = false, - hasContextRegisters = false, - isExpanded = false, - hasAssembly = false, - emptySourceNotation = false, - hasScmSourceContext = false, - registers, - frame, - event, - className, - frameMeta, - registersMeta, - platform, -}: Props) { - const organization = useOrganization(); - - const {projects} = useProjects(); - const project = useMemo( - () => projects.find(p => p.id === event.projectID), - [projects, event] - ); - - const shouldFetchSourceContext = - hasScmSourceContext && - defined(project) && - !hasContextSource && - isExpanded && - hasPotentialSourceContext(frame); - - const {data: sourceContextData, isPending: isLoadingSourceContext} = useSourceContext( - { - event, - frame, - orgSlug: organization?.slug || '', - projectSlug: project?.slug, - }, - {enabled: shouldFetchSourceContext} - ); - - const scmContext: Frame['context'] | undefined = useMemo(() => { - if (!sourceContextData?.context?.length) { - return; - } - return sourceContextData.context; - }, [sourceContextData]); - - // Use SCM-fetched context when the frame has no embedded context - const effectiveContext = hasContextSource ? frame?.context : scmContext; - const effectiveHasContextSource = hasContextSource || !!scmContext?.length; - - /** - * frame.lineNo is the highlighted frame in the middle of the context - */ - const activeLineNumber = frame.lineNo; - const contextLines = isExpanded - ? effectiveContext - : effectiveContext?.filter(l => l[0] === activeLineNumber); - - const fileExtension = getFileExtension(frame.filename || frame.absPath || '') ?? ''; - const lines = usePrismTokensSourceContext({ - contextLines, - lineNo: frame.lineNo, - fileExtension, - }); - - const isLoadingScmContext = shouldFetchSourceContext && isLoadingSourceContext; - - if ( - !isLoadingScmContext && - !effectiveHasContextSource && - !hasContextVars && - !hasContextRegisters && - !hasAssembly - ) { - return emptySourceNotation ? ( - - - {t('No additional details are available for this frame.')} - - ) : null; - } - - const startLineNo = effectiveHasContextSource ? (effectiveContext?.[0]?.[0] ?? 0) : 0; - - const prismClassName = fileExtension ? `language-${fileExtension}` : ''; - - return ( - - {isLoadingScmContext ? ( - - - {t('Loading source context…')} - - ) : effectiveContext && lines.length > 0 ? ( - -
-            
-              {lines.map((line, i) => {
-                const contextLine = contextLines![i]!;
-                const isActive = activeLineNumber === contextLine[0];
-
-                return (
-                  
-                    
-                      
-                      
-                        {line.map((token, key) => (
-                          
-                            {token.children}
-                          
-                        ))}
-                      
-                    
-                  
-                );
-              })}
-            
-          
-
- ) : null} - - {hasContextVars && ( - - - - )} - - {hasContextRegisters && ( - - )} - - {hasAssembly && } -
- ); -} - -const StyledClippedBox = styled(ClippedBox)` - padding: 0; -`; - -const StyledIconFlag = styled(IconFlag)` - margin-right: ${p => p.theme.space.md}; -`; - -const Wrapper = styled('ol')<{startLineNo: number}>` - counter-reset: frame ${p => p.startLineNo - 1}; - - && { - border-radius: 0 !important; - } -`; - -const CodeWrapper = styled('div')` - position: relative; - padding: 0; - - && pre, - && code { - font-size: ${p => p.theme.font.size.sm}; - white-space: pre-wrap; - margin: 0; - overflow: hidden; - background: ${p => p.theme.tokens.background.primary}; - padding: 0; - border-radius: 0; - } -`; - -const EmptyContext = styled('div')` - display: flex; - align-items: center; - gap: ${p => p.theme.space.md}; - padding: 20px; - color: ${p => p.theme.tokens.content.secondary}; - font-size: ${p => p.theme.font.size.md}; -`; - -const ContextLineWrapper = styled('div')<{isActive: boolean}>` - display: grid; - grid-template-columns: 58px 1fr; - gap: ${p => p.theme.space.md}; - background: ${p => - p.isActive ? 'var(--prism-highlight-background)' : p.theme.tokens.background.primary}; - padding-right: ${p => p.theme.space.xl}; -`; - -const ContextLineCode = styled('div')` - line-height: 24px; - white-space: pre-wrap; - vertical-align: middle; -`; diff --git a/static/app/components/events/interfaces/frame/contextLineNumber.tsx b/static/app/components/events/interfaces/frame/contextLineNumber.tsx deleted file mode 100644 index 14d8b5e41b85..000000000000 --- a/static/app/components/events/interfaces/frame/contextLineNumber.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import styled from '@emotion/styled'; - -interface Props { - isActive: boolean; - lineNumber: number; - children?: React.ReactNode; -} - -export function ContextLineNumber({lineNumber, isActive}: Props) { - return ( - -
{lineNumber}
-
- ); -} - -const Wrapper = styled('div')` - background: inherit; - height: 24px; - width: 58px; - display: inline-block; - color: ${p => p.theme.tokens.content.primary}; - font-size: ${p => p.theme.font.size.sm}; - margin-right: ${p => p.theme.space.md}; - - .line-number { - display: flex; - align-items: center; - flex-direction: row; - flex-wrap: nowrap; - justify-content: end; - height: 100%; - text-align: right; - padding-right: ${p => p.theme.space.xl}; - margin-right: ${p => p.theme.space.lg}; - background: transparent; - min-width: 58px; - border-right: 3px solid transparent; - user-select: none; - } - - &.active { - background: none; - } -`; diff --git a/static/app/components/events/interfaces/frame/defaultTitle/index.tsx b/static/app/components/events/interfaces/frame/defaultTitle/index.tsx deleted file mode 100644 index 85f4c361c132..000000000000 --- a/static/app/components/events/interfaces/frame/defaultTitle/index.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import {Fragment} from 'react'; -import styled from '@emotion/styled'; - -import {ExternalLink} from '@sentry/scraps/link'; -import {Tooltip} from '@sentry/scraps/tooltip'; - -import {openNavigateToExternalLinkModal} from 'sentry/actionCreators/modal'; -import {FunctionName} from 'sentry/components/events/interfaces/frame/functionName'; -import { - getPlatform, - isDotnet, - trimPackage, -} from 'sentry/components/events/interfaces/frame/utils'; -import {AnnotatedText} from 'sentry/components/events/meta/annotatedText'; -import {QuestionTooltip} from 'sentry/components/questionTooltip'; -import {Truncate} from 'sentry/components/truncate'; -import {SLOW_TOOLTIP_DELAY} from 'sentry/constants'; -import {IconOpen, IconQuestion} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import type {Frame} from 'sentry/types/event'; -import type {Meta} from 'sentry/types/group'; -import type {PlatformKey} from 'sentry/types/platform'; -import {defined} from 'sentry/utils/defined'; -import {isValidUrl} from 'sentry/utils/string/isValidUrl'; - -/** - * File paths can get very long, so increase it for the tooltips within this component. - */ -export const FRAME_TOOLTIP_MAX_WIDTH = 750; - -type Props = { - frame: Frame; - platform: PlatformKey; - /** - * Is the stack trace being previewed in a hovercard? - */ - isHoverPreviewed?: boolean; - /** - * Determines if the frame potentially originates from a third party - */ - isPotentiallyThirdParty?: boolean; - meta?: Record; -}; - -type GetPathNameOutput = {key: string; value: string; meta?: Meta}; - -export function DefaultTitle({ - frame, - platform, - isHoverPreviewed, - meta, - isPotentiallyThirdParty, -}: Props) { - const title: React.ReactElement[] = []; - const framePlatform = getPlatform(frame.platform, platform); - const tooltipDelay = isHoverPreviewed ? SLOW_TOOLTIP_DELAY : undefined; - - const handleExternalLink = (event: React.MouseEvent) => { - event.stopPropagation(); - if (isPotentiallyThirdParty && frame.absPath && isValidUrl(frame.absPath)) { - event.preventDefault(); - openNavigateToExternalLinkModal({linkText: frame.absPath}); - } - }; - - const getModule = (): GetPathNameOutput | undefined => { - if (frame.module) { - return { - key: 'module', - value: frame.module, - meta: meta?.module?.[''], - }; - } - - return undefined; - }; - - const getPathNameOrModule = ( - shouldPrioritizeModuleName: boolean - ): GetPathNameOutput | undefined => { - if (shouldPrioritizeModuleName) { - if (frame.module) { - return getModule(); - } - if (frame.filename) { - return { - key: 'filename', - value: frame.filename, - meta: meta?.filename?.[''], - }; - } - return undefined; - } - - if (frame.filename) { - return { - key: 'filename', - value: frame.filename, - meta: meta?.filename?.[''], - }; - } - - if (frame.module) { - return getModule(); - } - - return undefined; - }; - - // TODO(dcramer): this needs to use a formatted string so it can be - // localized correctly - if (defined(frame.filename || frame.module)) { - // prioritize module name for Java as filename is often only basename - const shouldPrioritizeModuleName = framePlatform === 'java'; - - const pathNameOrModule = getPathNameOrModule(shouldPrioritizeModuleName); - const enablePathTooltip = - defined(frame.absPath) && frame.absPath !== pathNameOrModule?.value; - - if (pathNameOrModule) { - title.push( - - - {isPotentiallyThirdParty && frame.absPath ? ( - - ) : !!pathNameOrModule.meta && !pathNameOrModule.value ? ( - - ) : ( - - )} - - - ); - } - - // in case we prioritized the module name but we also have a filename info - // we want to show a litle (?) icon that on hover shows the actual filename - if (shouldPrioritizeModuleName && frame.filename) { - title.push( - - - - - - ); - } - - if (frame.absPath && isValidUrl(frame.absPath)) { - title.push( - - - - ); - } - - if ( - (defined(frame.function) || defined(frame.rawFunction)) && - defined(pathNameOrModule) - ) { - title.push( - - {` ${t('in')} `} - - ); - } - } - - if (defined(frame.function) || defined(frame.rawFunction)) { - title.push( - - ); - } - - // we don't want to render out zero line numbers which are used to - // indicate lack of source information for native setups. We could - // TODO(mitsuhiko): only do this for events from native platforms? - if (defined(frame.lineNo) && frame.lineNo !== 0) { - title.push( - - {` ${t('at line')} `} - - ); - title.push( - - {defined(frame.colNo) ? `${frame.lineNo}:${frame.colNo}` : frame.lineNo} - - ); - } - - if (defined(frame.package) && !isDotnet(framePlatform)) { - title.push({` ${t('within')} `}); - title.push( - - {trimPackage(frame.package)} - - ); - } - - if (defined(frame.origAbsPath) && (frame.mapUrl || frame.map)) { - const text = (frame.mapUrl ?? frame.map)!; - title.push( - -
- {t('Source Map')} -
- {text} - - } - /> - ); - } - - return {title}; -} - -const StyledExternalLink = styled(ExternalLink)` - position: relative; - top: ${p => p.theme.space['2xs']}; - margin-left: ${p => p.theme.space.xs}; -`; - -const InFramePosition = styled('span')` - color: ${p => p.theme.tokens.content.primary}; - opacity: 0.6; -`; - -const StyledQuestionTooltip = styled(QuestionTooltip)` - margin-left: ${p => p.theme.space.xs}; -`; diff --git a/static/app/components/events/interfaces/frame/deprecatedLine.spec.tsx b/static/app/components/events/interfaces/frame/deprecatedLine.spec.tsx deleted file mode 100644 index c25ef023628a..000000000000 --- a/static/app/components/events/interfaces/frame/deprecatedLine.spec.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import {EventFixture} from 'sentry-fixture/event'; - -import {render, screen, within} from 'sentry-test/reactTestingLibrary'; - -import {DeprecatedLine} from 'sentry/components/events/interfaces/frame/deprecatedLine'; -import type {Frame} from 'sentry/types/event'; -import {EntryType} from 'sentry/types/event'; - -describe('Frame - Line', () => { - const event = EventFixture(); - - const data: Frame = { - absPath: null, - colNo: null, - context: [], - filename: null, - function: null, - inApp: false, - instructionAddr: null, - lineNo: null, - module: null, - package: null, - platform: null, - rawFunction: null, - symbol: null, - symbolAddr: null, - trust: null, - vars: null, - }; - - const defaultProps = { - platform: 'javascript', - emptySourceNotation: false, - hiddenFrameCount: 0, - frameMeta: {}, - hideSourceMapDebugger: false, - isHoverPreviewed: false, - lockAddress: undefined, - nextFrame: undefined, - frameSourceResolutionResults: undefined, - timesRepeated: 0, - isANR: false, - threadId: undefined, - registers: {}, - } satisfies Partial>; - - describe('renderOriginalSourceInfo()', () => { - it('should render the source map information as a HTML string', () => { - render( - - ); - }); - }); - - describe('renderContext()', () => { - it('should render context lines', () => { - render( - - ); - }); - - it('should render register values', () => { - render( - - ); - expect(screen.getByText('Registers')).toBeInTheDocument(); - }); - - it('should not render empty registers', () => { - render(); - - expect(screen.queryByText('Registers')).not.toBeInTheDocument(); - }); - - it('should render context vars', () => { - const vars = { - origin: null, - helper: '', - self: '', - args: [], - request: '', - content: '[Filtered]', - kwargs: {}, - project_id: "u'3'", - }; - - render( - - ); - - for (const [key, value] of Object.entries(vars)) { - const row = screen.getByText(key).closest('tr'); - expect(row).toBeTruthy(); - - if (!row) { - return; - } - - const utils = within(row); - expect(utils.getByText(key)).toBeInTheDocument(); - - if (typeof value !== 'string') { - return; - } - - expect(utils.getByText(value)).toBeInTheDocument(); - } - }); - }); - - describe('ANR suspect frame', () => { - it('should render suspect frame', () => { - const eventWithThreads = EventFixture({ - entries: [ - { - data: { - values: [ - { - id: 13920, - current: true, - crashed: true, - name: 'puma 002', - stacktrace: null, - rawStacktrace: null, - state: 'WAITING', - }, - ], - }, - type: EntryType.THREADS, - }, - ], - }); - const suspectFrame: Frame = { - filename: 'Instrumentation.java', - absPath: 'Instrumentation.java', - module: 'android.app.Instrumentation', - package: null, - platform: null, - instructionAddr: null, - symbolAddr: null, - function: 'callApplicationOnCreate', - rawFunction: null, - symbol: null, - context: [], - lineNo: 1176, - colNo: null, - inApp: false, - trust: null, - vars: null, - }; - - render( - - ); - expect(screen.getByText('Suspect Frame')).toBeInTheDocument(); - }); - }); -}); diff --git a/static/app/components/events/interfaces/frame/deprecatedLine.tsx b/static/app/components/events/interfaces/frame/deprecatedLine.tsx deleted file mode 100644 index 0a53e304d01e..000000000000 --- a/static/app/components/events/interfaces/frame/deprecatedLine.tsx +++ /dev/null @@ -1,475 +0,0 @@ -import {Fragment, useMemo, useState} from 'react'; -import {css} from '@emotion/react'; -import styled from '@emotion/styled'; -import classNames from 'classnames'; - -import {Tag} from '@sentry/scraps/badge'; -import {Button} from '@sentry/scraps/button'; -import InteractionStateLayer from '@sentry/scraps/interactionStateLayer'; -import {useModal} from '@sentry/scraps/modal'; - -import {ErrorBoundary} from 'sentry/components/errorBoundary'; -import {analyzeFrameForRootCause} from 'sentry/components/events/interfaces/analyzeFrames'; -import {LeadHint} from 'sentry/components/events/interfaces/frame/leadHint'; -import {StacktraceLink} from 'sentry/components/events/interfaces/frame/stacktraceLink'; -import type {FrameSourceMapDebuggerData} from 'sentry/components/events/interfaces/sourceMapsDebuggerModal'; -import {SourceMapsDebuggerModal} from 'sentry/components/events/interfaces/sourceMapsDebuggerModal'; -import {useStacktraceContext} from 'sentry/components/events/interfaces/stackTraceContext'; -import {getThreadById} from 'sentry/components/events/interfaces/utils'; -import {StrictClick} from 'sentry/components/strictClick'; -import {IconChevron, IconFix, IconRefresh} from 'sentry/icons'; -import {t, tn} from 'sentry/locale'; -import type {Event, Frame} from 'sentry/types/event'; -import type {SentryAppSchemaStacktraceLink} from 'sentry/types/integrations'; -import type {PlatformKey} from 'sentry/types/platform'; -import type {StacktraceType} from 'sentry/types/stacktrace'; -import {trackAnalytics} from 'sentry/utils/analytics'; -import {useOrganization} from 'sentry/utils/useOrganization'; -import {useSentryAppComponentsStore} from 'sentry/utils/useSentryAppComponentsStore'; -import {SectionKey} from 'sentry/views/issueDetails/context'; - -import {Context} from './context'; -import {DefaultTitle} from './defaultTitle'; -import {OpenInContextLine} from './openInContextLine'; -import { - getPlatform, - hasAssembly, - hasContextRegisters, - hasContextSource, - hasContextVars, - hasPotentialSourceContext, - isPotentiallyThirdPartyFrame, -} from './utils'; - -const VALID_SOURCE_MAP_DEBUGGER_FILE_ENDINGS = [ - '.js', - '.mjs', - '.cjs', - '.jsbundle', // React Native iOS file ending - '.bundle', // React Native Android file ending - '.hbc', // Hermes Bytecode (from Expo updates) file ending - '.js.gz', // file ending idiomatic for Ember.js -]; - -export interface DeprecatedLineProps { - data: Frame; - emptySourceNotation: boolean; - event: Event; - frameMeta: Record; - frameSourceResolutionResults: FrameSourceMapDebuggerData | undefined; - hiddenFrameCount: number | undefined; - hideSourceMapDebugger: boolean; - isANR: boolean; - isExpanded: boolean; - /** - * Is the stack trace being previewed in a hovercard? - */ - isHoverPreviewed: boolean; - lockAddress: string | undefined; - nextFrame: Frame | undefined; - platform: PlatformKey; - registers: StacktraceType['registers']; - threadId: number | undefined; - timesRepeated: number; - isShowFramesToggleExpanded?: boolean; - /** - * Frames that are hidden under the most recent non-InApp frame - */ - isSubFrame?: boolean; - onShowFramesToggle?: (event: React.MouseEvent) => void; - registersMeta?: Record; -} - -export function DeprecatedLine({ - data, - emptySourceNotation, - event, - frameMeta, - frameSourceResolutionResults, - hiddenFrameCount, - hideSourceMapDebugger, - isANR, - isExpanded: initialExpanded, - isHoverPreviewed, - lockAddress, - nextFrame, - platform: propPlatform, - registers, - threadId, - timesRepeated, - isShowFramesToggleExpanded, - isSubFrame, - onShowFramesToggle, - registersMeta, -}: DeprecatedLineProps) { - const components = useSentryAppComponentsStore({ - componentType: 'stacktrace-link', - }); - const {openModal} = useModal(); - - const organization = useOrganization(); - const {hasScmSourceContext} = useStacktraceContext(); - const [isHovering, setIsHovering] = useState(false); - const [isExpanded, setIsExpanded] = useState(initialExpanded ?? false); - const platform = getPlatform(data.platform, propPlatform ?? 'other'); - const leadsToApp = !data.inApp && (nextFrame?.inApp || !nextFrame); - - const isExpandable = useMemo((): boolean => { - return !!( - (hasContextSource(data) && data.context) || - hasContextVars(data) || - hasContextRegisters(registers) || - hasAssembly(data, platform) || - (hasScmSourceContext && hasPotentialSourceContext(data)) - ); - }, [data, registers, platform, hasScmSourceContext]); - - const toggleContext = (evt?: React.MouseEvent) => { - evt?.preventDefault(); - setIsExpanded(!isExpanded); - }; - - const handleMouseEnter = () => { - setIsHovering(true); - }; - - const handleMouseLeave = () => { - setIsHovering(false); - }; - - const scrollToSuspectRootCause = (e: React.MouseEvent) => { - e.stopPropagation(); // to prevent collapsing if collapsible - document - .getElementById(SectionKey.SUSPECT_ROOT_CAUSE) - ?.scrollIntoView({block: 'start', behavior: 'smooth'}); - }; - - const anrCulprit = - isANR && analyzeFrameForRootCause(data, getThreadById(event, threadId), lockAddress); - - const frameHasValidFileEndingForSourceMapDebugger = - VALID_SOURCE_MAP_DEBUGGER_FILE_ENDINGS.some( - ending => - (data.absPath ?? '').endsWith(ending) || (data.filename ?? '').endsWith(ending) - ); - - // If context is available (non-empty), users can already see the source code - // This means they have a "good stack trace" with readable source lines - // In this case, we want to hide the 'unminify code' button since the - // user already has sufficient debugging information - const shouldShowSourceMapDebuggerButton = - !hasContextSource(data) && - !hideSourceMapDebugger && - data.inApp && - frameHasValidFileEndingForSourceMapDebugger && - frameSourceResolutionResults && - !frameSourceResolutionResults.frameIsResolved; - - const sourceMapDebuggerAmplitudeData = { - organization: organization ?? null, - project_id: event.projectID, - event_id: event.id, - event_platform: event.platform, - sdk_name: event.sdk?.name, - sdk_version: event.sdk?.version, - }; - - const activeLineNumber = data.lineNo; - const contextLine = (data?.context || []).find((l: any) => l[0] === activeLineNumber); - // InApp or .NET because of: https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/sourcelink - const hasStacktraceLink = - (data.inApp || event.platform === 'csharp') && - !!data.filename && - (isHovering || isExpanded); - const showSentryAppStacktraceLinkInFrame = hasStacktraceLink && components.length > 0; - - const className = classNames({ - frame: true, - 'is-expandable': isExpandable, - expanded: isExpanded, - collapsed: !isExpanded, - 'system-frame': !data.inApp, - 'leads-to-app': leadsToApp, - }); - - return ( -
  • - - - {isExpandable ? : null} - - -
    - - -
    -
    -
    - - - {anrCulprit ? ( - - {t('Suspect Frame')} - - ) : null} - {hasStacktraceLink && !shouldShowSourceMapDebuggerButton && ( - - - - )} - {showSentryAppStacktraceLinkInFrame && ( - - - - )} - {hiddenFrameCount ? ( - { - onShowFramesToggle?.(e); - }} - > - {isShowFramesToggleExpanded - ? tn('Hide %s more frame', 'Hide %s more frames', hiddenFrameCount) - : tn('Show %s more frame', 'Show %s more frames', hiddenFrameCount)} - - ) : null} - {shouldShowSourceMapDebuggerButton ? ( - - { - e.stopPropagation(); - - trackAnalytics( - 'source_map_debug_blue_thunder.modal_opened', - sourceMapDebuggerAmplitudeData - ); - - openModal( - modalProps => ( - - ), - { - modalCss: css` - max-width: 800px; - width: 100%; - `, - onClose: () => { - trackAnalytics( - 'source_map_debug_blue_thunder.modal_closed', - sourceMapDebuggerAmplitudeData - ); - }, - } - ); - }} - > - - - {t('Unminify Code')} - - - - ) : null} - {data.inApp ? {t('In App')} : null} - {isExpandable ? ( - - - - ) : ( -
    - )} - - - - -
  • - ); -} - -function RepeatsIndicator({timesRepeated}: {timesRepeated: number}) { - if (!timesRepeated || timesRepeated <= 0) { - return null; - } - - return ( - - - - {timesRepeated} - - - ); -} - -const RepeatedFrames = styled('div')` - display: inline-block; -`; - -const DefaultLineTitleWrapper = styled('div')<{isInAppFrame: boolean}>` - display: flex; - align-items: center; - justify-content: space-between; - min-width: 0; - flex: 1; - color: ${p => (p.isInAppFrame ? '' : p.theme.tokens.content.secondary)}; - font-style: ${p => (p.isInAppFrame ? '' : 'italic')}; -`; - -const LeftLineTitle = styled('div')` - display: flex; - align-items: center; - min-width: 0; -`; - -const RepeatedContent = styled(LeftLineTitle)` - justify-content: center; -`; - -const FrameActions = styled('div')` - display: flex; - align-items: center; - gap: ${p => p.theme.space.md}; - flex-shrink: 0; - margin-left: auto; -`; - -const DefaultLine = styled('div')<{ - isExpandable: boolean; - isExpanded: boolean; - isSubFrame: boolean; -}>` - position: relative; - display: flex; - justify-content: space-between; - align-items: center; - background: ${p => - p.isSubFrame ? p.theme.colors.surface200 : p.theme.tokens.background.tertiary}; - min-height: 40px; - word-break: break-word; - padding: ${p => p.theme.space.sm} ${p => p.theme.space.lg}; - font-size: ${p => p.theme.font.size.sm}; - line-height: 16px; - cursor: ${p => (p.isExpandable ? 'pointer' : 'default')}; - code { - font-family: ${p => p.theme.font.family.sans}; - } - - @media (max-width: ${p => p.theme.breakpoints.sm}) { - &:has([data-has-setup]) { - flex-wrap: wrap; - row-gap: ${p => p.theme.space.xs}; - - > ${DefaultLineTitleWrapper} { - flex-basis: 100%; - } - - > ${FrameActions} { - flex-basis: 100%; - justify-content: flex-end; - flex-wrap: wrap; - row-gap: ${p => p.theme.space.xs}; - } - } - } -`; - -const StyledIconRefresh = styled(IconRefresh)` - margin-right: ${p => p.theme.space['2xs']}; -`; - -const ToggleContextButton = styled(Button)` - color: ${p => p.theme.tokens.content.secondary}; -`; - -const ToggleButton = styled(Button)` - color: ${p => p.theme.tokens.content.secondary}; - font-size: ${p => p.theme.font.size.sm}; - font-style: italic; - font-weight: ${p => p.theme.font.weight.sans.regular}; - padding: ${p => p.theme.space['2xs']} ${p => p.theme.space.xs}; - - &:hover { - color: ${p => p.theme.tokens.content.secondary}; - } -`; - -const SourceMapDebuggerButtonText = styled('span')` - margin-left: ${p => p.theme.space.xs}; -`; - -const SourceMapDebuggerModalButton = styled(Button)` - height: 20px; - padding: 0 ${p => p.theme.space.sm}; - font-size: ${p => p.theme.font.size.sm}; -`; diff --git a/static/app/components/events/interfaces/frame/frameVariables.spec.tsx b/static/app/components/events/interfaces/frame/frameVariables.spec.tsx deleted file mode 100644 index 0630f13abed0..000000000000 --- a/static/app/components/events/interfaces/frame/frameVariables.spec.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import {DataScrubbingRelayPiiConfigFixture} from 'sentry-fixture/dataScrubbingRelayPiiConfig'; -import {OrganizationFixture} from 'sentry-fixture/organization'; -import {DetailedProjectFixture} from 'sentry-fixture/project'; - -import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; -import {textWithMarkupMatcher} from 'sentry-test/utils'; - -import {FrameVariables} from 'sentry/components/events/interfaces/frame/frameVariables'; -import {ProjectsStore} from 'sentry/stores/projectsStore'; - -describe('Frame Variables', () => { - it('renders', async () => { - const organization = OrganizationFixture(); - const project = DetailedProjectFixture({id: '0'}); - const projectDetails = DetailedProjectFixture({ - ...project, - relayPiiConfig: JSON.stringify(DataScrubbingRelayPiiConfigFixture()), - }); - const initialRouterConfig = { - location: { - pathname: '/organizations/org-slug/issues/1/', - query: {project: project.id}, - }, - route: '/organizations/:orgId/issues/:groupId/', - }; - MockApiClient.addMockResponse({ - url: `/projects/org-slug/${project.slug}/`, - body: projectDetails, - }); - ProjectsStore.loadInitialData([project]); - - render( - , - { - organization, - initialRouterConfig, - } - ); - - expect(screen.getAllByText(/redacted/)).toHaveLength(2); - - await userEvent.hover(screen.getAllByText(/redacted/)[0]!); - - expect( - await screen.findByText( - textWithMarkupMatcher( - 'Replaced because of the data scrubbing rule [Replace] [Password fields] with [Scrubbed] from [password] in the settings of the project project-slug' - ) - ) - ).toBeInTheDocument(); // tooltip description - - expect( - screen.getByRole('link', { - name: '[Replace] [Password fields] with [Scrubbed] from [password]', - }) - ).toHaveAttribute( - 'href', - '/settings/org-slug/projects/project-slug/security-and-privacy/advanced-data-scrubbing/0/' - ); - - expect(screen.getByRole('link', {name: 'project-slug'})).toHaveAttribute( - 'href', - '/settings/org-slug/projects/project-slug/security-and-privacy/' - ); - }); - - it('renders python variables correctly', () => { - render( - ', - }} - platform="python" - /> - ); - - expect( - within(screen.getByTestId('value-null')).getByText('None') - ).toBeInTheDocument(); - expect( - within(screen.getByTestId('value-boolean')).getByText('True') - ).toBeInTheDocument(); - expect( - within(screen.getByTestId('value-string')).getByText('"string"') - ).toBeInTheDocument(); - expect( - within(screen.getByTestId('value-number')).getByText('123.45') - ).toBeInTheDocument(); - expect( - within(screen.getByTestId('value-unformatted')).getByText('') - ).toBeInTheDocument(); - }); - - it('renders node variables correctly', () => { - render( - ', - undefined: '', - bool: true, - number: 123.45, - str: 'string', - }} - platform="node" - /> - ); - - const nullValues = screen.getAllByTestId('value-null'); - - expect(within(nullValues[0]!).getByText('null')).toBeInTheDocument(); - expect(within(nullValues[1]!).getByText('undefined')).toBeInTheDocument(); - expect( - within(screen.getByTestId('value-boolean')).getByText('true') - ).toBeInTheDocument(); - expect( - within(screen.getByTestId('value-number')).getByText('123.45') - ).toBeInTheDocument(); - expect( - within(screen.getByTestId('value-unformatted')).getByText('string') - ).toBeInTheDocument(); - }); - - it('renders ruby variables correctly', () => { - render( - - ); - - expect(within(screen.getByTestId('value-null')).getByText('nil')).toBeInTheDocument(); - expect( - within(screen.getByTestId('value-boolean')).getByText('true') - ).toBeInTheDocument(); - expect( - within(screen.getByTestId('value-unformatted')).getByText('string') - ).toBeInTheDocument(); - }); - - it('renders php variables correctly', () => { - render( - - ); - - expect( - within(screen.getByTestId('value-null')).getByText('null') - ).toBeInTheDocument(); - expect( - within(screen.getByTestId('value-boolean')).getByText('true') - ).toBeInTheDocument(); - expect( - within(screen.getByTestId('value-unformatted')).getByText('string') - ).toBeInTheDocument(); - }); -}); diff --git a/static/app/components/events/interfaces/frame/frameVariables.tsx b/static/app/components/events/interfaces/frame/frameVariables.tsx deleted file mode 100644 index 40afa19b18fa..000000000000 --- a/static/app/components/events/interfaces/frame/frameVariables.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import {useMemo} from 'react'; - -import {KeyValueList} from 'sentry/components/events/interfaces/keyValueList'; -import type {StructedEventDataConfig} from 'sentry/components/structuredEventData'; -import {StructuredEventData} from 'sentry/components/structuredEventData'; -import type {KeyValueListData} from 'sentry/types/group'; -import type {PlatformKey} from 'sentry/types/platform'; - -type Props = { - data: Record< - string, - string | null | boolean | number | Record - > | null; - meta?: Record; - platform?: PlatformKey; -}; - -const PYTHON_STRING_REGEX = /^['"](.*)['"]$/; -const NUMERIC_STRING_REGEX = /^-?\d+(\.\d+)?$/; - -const renderPythonBoolean = (value: unknown) => { - if (typeof value === 'string') { - return value; - } - - return value ? 'True' : 'False'; -}; - -const renderNodeNull = (value: unknown) => { - if (value === '') { - return 'null'; - } - - if (value === '') { - return 'undefined'; - } - - return String(value); -}; - -const getStructuredDataConfig = ({ - platform, -}: { - platform?: PlatformKey; -}): StructedEventDataConfig => { - switch (platform) { - case 'python': - return { - isBoolean: value => - typeof value === 'boolean' || value === 'True' || value === 'False', - isNull: value => value === null || value === 'None', - renderBoolean: renderPythonBoolean, - renderNull: () => 'None', - // Python SDK wraps string values in single quotes - isString: value => typeof value === 'string' && PYTHON_STRING_REGEX.test(value), - // Strip single quotes from python strings for display purposes - renderString: value => value.replace(PYTHON_STRING_REGEX, '$1'), - // Python SDK returns numbers as strings, but we can assume they are numbers if they look like one - isNumber: value => - typeof value === 'number' || - (typeof value === 'string' && NUMERIC_STRING_REGEX.test(value)), - }; - case 'ruby': - return { - isBoolean: value => - typeof value === 'boolean' || value === 'true' || value === 'false', - isNull: value => value === null || value === 'nil', - renderNull: () => 'nil', - }; - case 'php': - return { - isBoolean: value => - typeof value === 'boolean' || value === 'true' || value === 'false', - isNull: value => value === null || value === 'null', - }; - case 'node': - return { - isNull: value => value === null || value === '' || value === '', - renderNull: renderNodeNull, - }; - default: - return {}; - } -}; - -export function FrameVariables({data, meta, platform}: Props) { - const transformedData = useMemo(() => { - const config = getStructuredDataConfig({platform}); - if (!data) { - return []; - } - - return Object.keys(data) - .reverse() - .map(key => ({ - key, - subject: key, - value: ( - - ), - })); - }, [data, meta, platform]); - - return ; -} diff --git a/static/app/components/events/interfaces/frame/functionName.tsx b/static/app/components/events/interfaces/frame/functionName.tsx deleted file mode 100644 index 0c5b35f3604b..000000000000 --- a/static/app/components/events/interfaces/frame/functionName.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import {AnnotatedText} from 'sentry/components/events/meta/annotatedText'; -import type {getMeta} from 'sentry/components/events/meta/metaProxy'; -import {t} from 'sentry/locale'; -import type {Frame} from 'sentry/types/event'; - -type Props = { - frame: Frame; - className?: string; - meta?: Record; -}; - -export function FunctionName({frame, className, meta, ...props}: Props) { - const getValueOutput = (): - | {meta: ReturnType; value: Frame['function']} - | undefined => { - if (frame.function) { - return { - value: frame.function, - meta: meta?.function?.[''], - }; - } - - if (frame.rawFunction) { - return { - value: frame.rawFunction, - meta: meta?.rawFunction?.[''], - }; - } - - return undefined; - }; - - const valueOutput = getValueOutput(); - - return ( - - {valueOutput ? ( - - ) : ( - t('') - )} - - ); -} diff --git a/static/app/components/events/interfaces/frame/leadHint.tsx b/static/app/components/events/interfaces/frame/leadHint.tsx deleted file mode 100644 index 333af47b77f6..000000000000 --- a/static/app/components/events/interfaces/frame/leadHint.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import {getLeadHint} from 'sentry/components/events/interfaces/frame/utils'; -import type {Event, Frame} from 'sentry/types/event'; -import {defined} from 'sentry/utils/defined'; - -type Props = { - event: Event; - leadsToApp: boolean; - isExpanded?: boolean; - nextFrame?: Frame; -}; - -export function LeadHint({leadsToApp, isExpanded, nextFrame, event}: Props) { - if (isExpanded || !leadsToApp) { - return null; - } - - return ( -
    - {getLeadHint({event, hasNextFrame: defined(nextFrame)})} - {': '} -
    - ); -} diff --git a/static/app/components/events/interfaces/frame/utils.tsx b/static/app/components/events/interfaces/frame/utils.tsx index 03bbb941d3ff..80744e5d8727 100644 --- a/static/app/components/events/interfaces/frame/utils.tsx +++ b/static/app/components/events/interfaces/frame/utils.tsx @@ -29,7 +29,7 @@ export function hasContextSource(frame: Frame) { return defined(frame.context) && !!frame.context.length; } -export function hasContextVars(frame: Frame) { +function hasContextVars(frame: Frame) { return !isEmptyObject(frame.vars || {}); } diff --git a/static/app/components/events/interfaces/nativeFrame.tsx b/static/app/components/events/interfaces/nativeFrame.tsx deleted file mode 100644 index 47ead1b43e32..000000000000 --- a/static/app/components/events/interfaces/nativeFrame.tsx +++ /dev/null @@ -1,579 +0,0 @@ -import type {MouseEvent} from 'react'; -import {Fragment, useState} from 'react'; -import styled from '@emotion/styled'; - -import {Tag} from '@sentry/scraps/badge'; -import {Button} from '@sentry/scraps/button'; -import InteractionStateLayer from '@sentry/scraps/interactionStateLayer'; -import {Flex} from '@sentry/scraps/layout'; -import {Tooltip} from '@sentry/scraps/tooltip'; - -import {ErrorBoundary} from 'sentry/components/errorBoundary'; -import {FRAME_TOOLTIP_MAX_WIDTH} from 'sentry/components/events/interfaces/frame/defaultTitle'; -import {OpenInContextLine} from 'sentry/components/events/interfaces/frame/openInContextLine'; -import {StacktraceLink} from 'sentry/components/events/interfaces/frame/stacktraceLink'; -import { - getLeadHint, - hasAssembly, - hasContextRegisters, - hasContextSource, - hasContextVars, - isExpandable, - trimPackage, -} from 'sentry/components/events/interfaces/frame/utils'; -import {useStacktraceContext} from 'sentry/components/events/interfaces/stackTraceContext'; -import {formatAddress, parseAddress} from 'sentry/components/events/interfaces/utils'; -import {AnnotatedText} from 'sentry/components/events/meta/annotatedText'; -import {StrictClick} from 'sentry/components/strictClick'; -import {SLOW_TOOLTIP_DELAY} from 'sentry/constants'; -import {IconChevron} from 'sentry/icons'; -import {IconFileBroken} from 'sentry/icons/iconFileBroken'; -import {IconRefresh} from 'sentry/icons/iconRefresh'; -import {IconWarning} from 'sentry/icons/iconWarning'; -import {SvgIcon} from 'sentry/icons/svgIcon'; -import {t, tn} from 'sentry/locale'; -import type {Image} from 'sentry/types/debugImage'; -import type {Event, Frame} from 'sentry/types/event'; -import type {SentryAppSchemaStacktraceLink} from 'sentry/types/integrations'; -import type {PlatformKey} from 'sentry/types/platform'; -import {StackView, type StacktraceType} from 'sentry/types/stacktrace'; -import {defined} from 'sentry/utils/defined'; -import {useSentryAppComponentsStore} from 'sentry/utils/useSentryAppComponentsStore'; -import {useSyncedLocalStorageState} from 'sentry/utils/useSyncedLocalStorageState'; -import {SectionKey, useIssueDetails} from 'sentry/views/issueDetails/context'; -import {getFoldSectionKey} from 'sentry/views/issueDetails/foldSection'; - -import {useOptionalDebugMetaSearch} from './debugMeta/debugMetaSearchContext'; -import {combineStatus} from './debugMeta/utils'; -import {Context} from './frame/context'; -import {SymbolicatorStatus} from './types'; - -type Props = { - emptySourceNotation: boolean; - event: Event; - frame: Frame; - frameMeta: Record; - hiddenFrameCount: number | undefined; - image: Image | null; - isFirstInAppFrame: boolean; - /** - * Is the stack trace being previewed in a hovercard? - */ - isHoverPreviewed: boolean | undefined; - isShowFramesToggleExpanded: boolean; - /** - * Frames that are hidden under the most recent non-InApp frame - */ - isSubFrame: boolean; - isUsedForGrouping: boolean; - maxLengthOfRelativeAddress: number; - nextFrame: Frame; - onShowFramesToggle: (event: React.MouseEvent) => void; - platform: PlatformKey; - prevFrame: Frame | undefined; - registers: StacktraceType['registers']; - registersMeta: Record; -}; - -export function NativeFrame({ - frame, - nextFrame, - prevFrame, - isUsedForGrouping, - maxLengthOfRelativeAddress, - image, - registers, - event, - hiddenFrameCount, - isFirstInAppFrame, - isShowFramesToggleExpanded, - isSubFrame, - onShowFramesToggle, - platform, - registersMeta, - frameMeta, - emptySourceNotation, - isHoverPreviewed = false, -}: Props) { - const components = useSentryAppComponentsStore({ - componentType: 'stacktrace-link', - }); - const isDartAsyncSuspensionFrame = - frame.filename === '' || - frame.absPath === ''; - const {displayOptions, stackView, hasScmSourceContext} = useStacktraceContext(); - - const {sectionData} = useIssueDetails(); - const debugMetaSearch = useOptionalDebugMetaSearch(); - const debugSectionConfig = sectionData[SectionKey.DEBUGMETA]; - const [_isCollapsed, setIsCollapsed] = useSyncedLocalStorageState( - getFoldSectionKey(SectionKey.DEBUGMETA), - debugSectionConfig?.initialCollapse ?? false - ); - const fullStackTrace = stackView === StackView.FULL; - - const absolute = displayOptions.includes('absolute-addresses'); - const fullFunctionName = displayOptions.includes('verbose-function-names'); - const absoluteFilePaths = displayOptions.includes('absolute-file-paths'); - - const tooltipDelay = isHoverPreviewed ? SLOW_TOOLTIP_DELAY : undefined; - const foundByStackScanning = frame.trust === 'scan' || frame.trust === 'cfi-scan'; - const startingAddress = image ? image.image_addr : null; - const packageClickable = - !!frame.symbolicatorStatus && - frame.symbolicatorStatus !== SymbolicatorStatus.UNKNOWN_IMAGE && - !isHoverPreviewed && - !!debugSectionConfig && - !!debugMetaSearch; - - const leadsToApp = !frame.inApp && (nextFrame?.inApp || !nextFrame); - const expandable = isExpandable({ - frame, - registers, - platform, - emptySourceNotation, - hasScmSourceContext, - }); - - const inlineFrame = - prevFrame && - platform === (prevFrame.platform || platform) && - frame.instructionAddr === prevFrame.instructionAddr; - - const functionNameHiddenDetails = - defined(frame.rawFunction) && - defined(frame.function) && - frame.function !== frame.rawFunction; - - const [expanded, setExpanded] = useState(() => isFirstInAppFrame); - const [isHovering, setHovering] = useState(false); - - const contextLine = (frame?.context || []).find(l => l[0] === frame.lineNo); - const showStacktraceLink = frame.inApp && !!frame.filename && (isHovering || expanded); - const showSentryAppStacktraceLinkInFrame = showStacktraceLink && components.length > 0; - - const handleMouseEnter = () => setHovering(true); - - const handleMouseLeave = () => setHovering(false); - - function getRelativeAddress() { - if (!startingAddress) { - return ''; - } - - const relativeAddress = formatAddress( - parseAddress(frame.instructionAddr) - parseAddress(startingAddress), - maxLengthOfRelativeAddress - ); - - return `+${relativeAddress}`; - } - - function getAddressTooltip() { - if (inlineFrame && foundByStackScanning) { - return t('Inline frame, found by stack scanning'); - } - - if (inlineFrame) { - return t('Inline frame'); - } - - if (foundByStackScanning) { - return t('Found by stack scanning'); - } - - return; - } - - function getFunctionName() { - if (functionNameHiddenDetails && fullFunctionName && frame.rawFunction) { - return { - value: frame.rawFunction, - meta: frameMeta?.rawFunction?.[''], - }; - } - - if (frame.function) { - return { - value: frame.function, - meta: frameMeta?.function?.[''], - }; - } - - return; - } - - // this is the status of image that belongs to this frame - function getStatus() { - // Treat Dart asynchronous suspension frames as symbolicated - these are synthetic markers set by Dart - if (isDartAsyncSuspensionFrame) { - return 'success'; - } - - // If a matching debug image doesn't exist, fall back to symbolicator_status - if (!image) { - switch (frame.symbolicatorStatus) { - case SymbolicatorStatus.SYMBOLICATED: - return 'success'; - case SymbolicatorStatus.MISSING: - case SymbolicatorStatus.MALFORMED: - return 'error'; - case SymbolicatorStatus.UNKNOWN_IMAGE: - return frame.instructionAddr === '0x0' ? 'success' : 'error'; - case SymbolicatorStatus.MISSING_SYMBOL: - default: - return; - } - } - - const combinedStatus = combineStatus(image.debug_status, image.unwind_status); - - switch (combinedStatus) { - case 'unused': - return; - case 'found': - return 'success'; - default: - return 'error'; - } - } - - // This isn't possible when the page doesn't have the images loaded section - function handleGoToImagesLoaded(e: MouseEvent) { - e.stopPropagation(); // to prevent collapsing if collapsible - - if (frame.instructionAddr) { - const searchTerm = - !(!frame.addrMode || frame.addrMode === 'abs') && image - ? `${image.debug_id}!${frame.instructionAddr}` - : frame.instructionAddr; - - debugMetaSearch?.setSearchTerm(searchTerm); - } - - // Expand the section - setIsCollapsed(false); - - // Scroll to the section - document - .getElementById(SectionKey.DEBUGMETA) - ?.scrollIntoView({block: 'start', behavior: 'smooth'}); - } - - function handleToggleContext(e: MouseEvent) { - if (!expandable) { - return; - } - e.preventDefault(); - setExpanded(!expanded); - } - - const relativeAddress = getRelativeAddress(); - const addressTooltip = getAddressTooltip(); - const functionName = getFunctionName(); - const status = getStatus(); - - return ( - - - - {expandable ? : null} - - {status === 'error' ? ( - - - - ) : status === undefined ? ( - - - - ) : null} - -
    - {!fullStackTrace && !expanded && leadsToApp && ( - - - {getLeadHint({event, hasNextFrame: defined(nextFrame)})} - - - )} - - - {frame.package - ? trimPackage(frame.package) - : isDartAsyncSuspensionFrame - ? t('Dart async') - : `<${t('unknown')}>`} - - -
    - - - - {!relativeAddress || absolute ? frame.instructionAddr : relativeAddress} - - - - - {functionName ? ( - - - - ) : isDartAsyncSuspensionFrame ? ( - t('Dart') - ) : ( - `<${t('unknown')}>` - )}{' '} - {frame.filename && ( - - - {'('} - {absoluteFilePaths ? frame.absPath : frame.filename} - {frame.lineNo && `:${frame.lineNo}`} - {')'} - - - )} - - - {isUsedForGrouping && ( - - - - )} - - {hiddenFrameCount ? ( - { - onShowFramesToggle?.(e); - }} - > - {isShowFramesToggleExpanded - ? tn('Hide %s more frame', 'Hide %s more frames', hiddenFrameCount) - : tn('Show %s more frame', 'Show %s more frames', hiddenFrameCount)} - - ) : null} - - {showStacktraceLink && ( - - - - )} - {showSentryAppStacktraceLinkInFrame && ( - - - - )} - - {frame.inApp ? {t('In App')} : null} - - - - {expandable && ( - } - /> - )} - -
    -
    - {expanded && ( - - )} -
    - ); -} - -const AddressCell = styled('div')` - font-family: ${p => p.theme.font.family.mono}; - ${p => p.onClick && 'cursor: pointer'}; - ${p => p.onClick && 'color:' + p.theme.tokens.interactive.link.accent.rest}; -`; - -const FunctionNameCell = styled('div')` - word-break: break-all; - - @media (max-width: ${p => p.theme.breakpoints.sm}) { - grid-column: 2/6; - } -`; - -const GroupingCell = styled('div')` - @media (max-width: ${p => p.theme.breakpoints.sm}) { - grid-row: 2/3; - } -`; - -const TypeCell = styled('div')` - @media (max-width: ${p => p.theme.breakpoints.sm}) { - grid-column: 5/6; - grid-row: 1/2; - } -`; - -const ExpandCell = styled('div')` - @media (max-width: ${p => p.theme.breakpoints.sm}) { - grid-column: 6/7; - grid-row: 1/2; - } -`; - -const ToggleButton = styled(Button)` - display: block; - color: ${p => p.theme.tokens.content.secondary}; -`; - -const Registers = styled(Context)` - border-bottom: 1px solid ${p => p.theme.tokens.border.primary}; - padding: 0; - margin: 0; -`; - -const PackageNote = styled('div')` - color: ${p => p.theme.tokens.content.secondary}; - font-size: ${p => p.theme.font.size.xs}; -`; - -const Package = styled('span')` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - width: 100%; - padding-right: 2px; /* Needed to prevent text cropping with italic font */ -`; - -const FileName = styled('span')` - color: ${p => p.theme.tokens.content.secondary}; - border-bottom: 1px dashed ${p => p.theme.tokens.border.primary}; -`; - -const RowHeader = styled('span')<{ - expandable: boolean; - isInAppFrame: boolean; - isSubFrame: boolean; -}>` - position: relative; - display: grid; - grid-template-columns: auto 150px 120px 4fr repeat(3, auto) ${p => p.theme.space.xl}; /* Adjusted to account for the extra element */ - grid-template-rows: 1fr; /* Ensures a single row */ - align-items: center; - align-content: center; - column-gap: ${p => p.theme.space.md}; - background-color: ${p => - !p.isInAppFrame && p.isSubFrame - ? p.theme.colors.surface200 - : p.theme.tokens.background.secondary}; - font-size: ${p => p.theme.font.size.sm}; - padding: ${p => p.theme.space.md}; - color: ${p => (p.isInAppFrame ? '' : p.theme.tokens.content.secondary)}; - font-style: ${p => (p.isInAppFrame ? '' : 'italic')}; - ${p => p.expandable && 'cursor: pointer;'}; - - @media (min-width: ${p => p.theme.breakpoints.sm}) { - grid-template-columns: auto 150px 120px 4fr repeat(3, auto) ${p => p.theme.space.xl}; /* Matches the updated desktop layout */ - padding: ${p => p.theme.space.xs} ${p => p.theme.space.lg}; - min-height: 32px; - } -`; - -const StackTraceFrame = styled('li')` - :not(:last-child) { - ${RowHeader} { - border-bottom: 1px solid ${p => p.theme.tokens.border.primary}; - } - } -`; - -const SymbolicatorIcon = styled('div')` - width: ${() => SvgIcon.ICON_SIZES.sm}; -`; - -const ShowHideButton = styled(Button)` - color: ${p => p.theme.tokens.content.secondary}; - font-size: ${p => p.theme.font.size.sm}; - font-style: italic; - font-weight: ${p => p.theme.font.weight.sans.regular}; - padding: ${p => p.theme.space['2xs']} ${p => p.theme.space.xs}; - &:hover { - color: ${p => p.theme.tokens.content.secondary}; - } -`; diff --git a/static/app/components/events/interfaces/performance/anrRootCause.spec.tsx b/static/app/components/events/interfaces/performance/anrRootCause.spec.tsx index 4898d95bc649..19b4780dd4fa 100644 --- a/static/app/components/events/interfaces/performance/anrRootCause.spec.tsx +++ b/static/app/components/events/interfaces/performance/anrRootCause.spec.tsx @@ -149,7 +149,10 @@ const makeEventWithThreads = (threads: Thread[]): Event => { type: 'ZeroDivisionError', value: 'divided by 0', }, - tags: [{key: 'level', value: 'error'}], + tags: [ + {key: 'level', value: 'error'}, + {key: 'mechanism', value: 'ANR'}, + ], platform: 'other', dateReceived: '2021-10-28T12:28:22.318469Z', errors: [], @@ -251,12 +254,8 @@ describe('anrRootCause', () => { ) ) ).toBeInTheDocument(); - expect(screen.getByTestId('stack-trace-content')).toBeInTheDocument(); - expect( - screen.getByText(textWithMarkupMatcher('Thread.java in wait at line 10')) - ).toBeInTheDocument(); - expect( - screen.getByText(textWithMarkupMatcher('MainActivity.java in onCreate at line 366')) - ).toBeInTheDocument(); + expect(screen.getByText('Suspect Frame')).toBeInTheDocument(); + expect(screen.getByText('wait')).toBeInTheDocument(); + expect(screen.getByText('onCreate')).toBeInTheDocument(); }); }); diff --git a/static/app/components/events/interfaces/performance/anrRootCause.tsx b/static/app/components/events/interfaces/performance/anrRootCause.tsx index 081db8145b02..d8da39b0c7da 100644 --- a/static/app/components/events/interfaces/performance/anrRootCause.tsx +++ b/static/app/components/events/interfaces/performance/anrRootCause.tsx @@ -4,7 +4,6 @@ import styled from '@emotion/styled'; import {Link} from '@sentry/scraps/link'; import {analyzeFramesForRootCause} from 'sentry/components/events/interfaces/analyzeFrames'; -import {StackTraceContent} from 'sentry/components/events/interfaces/crashContent/stackTrace'; import {NoStackTraceMessage} from 'sentry/components/events/interfaces/noStackTraceMessage'; import {getThreadStacktrace} from 'sentry/components/events/interfaces/threads/threadSelector/getThreadStacktrace'; import { @@ -15,10 +14,11 @@ import { import {ShortId} from 'sentry/components/group/inboxBadges/shortId'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import {extractSelectionParameters} from 'sentry/components/pageFilters/parse'; +import {IssueStackTraceFrameList} from 'sentry/components/stackTrace/issueStackTrace/exceptionStackTrace'; +import {NativeStackTraceViewStateProvider} from 'sentry/components/stackTrace/native/nativeDisplayOptionsContext'; import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; import type {Organization} from 'sentry/types/organization'; -import {StackView} from 'sentry/types/stacktrace'; import {trackAnalytics} from 'sentry/utils/analytics'; import {defined} from 'sentry/utils/defined'; import {useLocation} from 'sentry/utils/useLocation'; @@ -113,14 +113,15 @@ export function AnrRootCause({event, organization}: Props) { {anrCulprit?.resources} {defined(stackTrace) ? ( - + + + ) : ( )} diff --git a/static/app/components/events/interfaces/stackTrace.tsx b/static/app/components/events/interfaces/stackTrace.tsx deleted file mode 100644 index 0bcdd21cd5c0..000000000000 --- a/static/app/components/events/interfaces/stackTrace.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { - StacktraceContext, - useStacktraceContext, -} from 'sentry/components/events/interfaces/stackTraceContext'; -import {TraceEventDataSection} from 'sentry/components/events/traceEventDataSection'; -import {t} from 'sentry/locale'; -import type {Event, ExceptionValue} from 'sentry/types/event'; -import {EntryType} from 'sentry/types/event'; -import type {Group} from 'sentry/types/group'; -import type {PlatformKey} from 'sentry/types/platform'; -import type {Project} from 'sentry/types/project'; - -import {StackTraceContent} from './crashContent/stackTrace'; -import {NoStackTraceMessage} from './noStackTraceMessage'; -import {isStacktraceNewestFirst} from './utils'; - -type Props = { - data: NonNullable; - event: Event; - projectSlug: Project['slug']; - groupingCurrentLevel?: Group['metadata']['current_level']; -}; - -function StackTraceContentWrapper({ - event, - data, - groupingCurrentLevel, - platform, -}: Pick & { - platform: PlatformKey; -}) { - const {isNewestFramesFirst, stackView} = useStacktraceContext(); - - const entryIndex = event.entries.findIndex( - eventEntry => eventEntry.type === EntryType.STACKTRACE - ); - const meta = event._meta?.entries?.[entryIndex]?.data; - - return ( - - ); -} - -export function StackTrace({projectSlug, event, data, groupingCurrentLevel}: Props) { - function getPlatform(): PlatformKey { - const framePlatform = data.frames?.find(frame => !!frame.platform); - return framePlatform?.platform ?? event.platform ?? 'other'; - } - - const platform = getPlatform(); - const stackTraceNotFound = !(data.frames ?? []).length; - - const hasNonAppFrames = !!data.frames?.some(frame => !frame.inApp); - - return ( - - - !!frame.rawFunction && - !!frame.function && - frame.rawFunction !== frame.function - ) - } - hasAbsoluteFilePaths={!!data.frames?.some(frame => !!frame.filename)} - hasAbsoluteAddresses={!!data.frames?.some(frame => !!frame.instructionAddr)} - hasNewestFirst={(data.frames ?? []).length > 1} - > - {stackTraceNotFound ? ( - - ) : ( - - )} - - - ); -} diff --git a/static/app/components/events/interfaces/stackTraceContext.spec.tsx b/static/app/components/events/interfaces/stackTraceContext.spec.tsx deleted file mode 100644 index 5c6acb323b4c..000000000000 --- a/static/app/components/events/interfaces/stackTraceContext.spec.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import {ProjectFixture} from 'sentry-fixture/project'; - -import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; - -import {StackType, StackView} from 'sentry/types/stacktrace'; -import {localStorageWrapper} from 'sentry/utils/localStorage'; - -import {StacktraceContext, useStacktraceContext} from './stackTraceContext'; - -describe('StacktraceContext', () => { - function ContextDisplay() { - const context = useStacktraceContext(); - return ( -
    -
    {String(context.isFullStackTrace)}
    -
    - {String(context.isNewestFramesFirst)} -
    -
    {context.stackView}
    -
    {context.stackType}
    - {context.displayOptions.length > 0 && ( -
    - {JSON.stringify(context.displayOptions)} -
    - )} -
    - ); - } - - function TestButton({ - action, - children, - }: { - action: (context: ReturnType) => void; - children: React.ReactNode; - }) { - const context = useStacktraceContext(); - return ; - } - - beforeEach(() => { - localStorageWrapper.clear(); - MockApiClient.addMockResponse({ - url: '/projects/org-slug/test-project/', - body: ProjectFixture({slug: 'test-project'}), - }); - }); - - it('provides default values', () => { - render( - - - - ); - - expect(screen.getByTestId('is-full-stack-trace')).toHaveTextContent('false'); - expect(screen.getByTestId('is-newest-frames-first')).toHaveTextContent('true'); - expect(screen.getByTestId('stack-view')).toHaveTextContent(StackView.APP); - expect(screen.getByTestId('stack-type')).toHaveTextContent(StackType.ORIGINAL); - }); - - it('applies custom props', () => { - render( - - - - ); - - expect(screen.getByTestId('is-full-stack-trace')).toHaveTextContent('true'); - expect(screen.getByTestId('is-newest-frames-first')).toHaveTextContent('false'); - expect(screen.getByTestId('stack-view')).toHaveTextContent(StackView.FULL); - }); - - it('enables toggling full stack trace', async () => { - render( - - - ctx.setIsFullStackTrace(true)}>Show Full - - ); - - expect(screen.getByTestId('stack-view')).toHaveTextContent(StackView.APP); - - await userEvent.click(screen.getByText('Show Full')); - - expect(screen.getByTestId('is-full-stack-trace')).toHaveTextContent('true'); - expect(screen.getByTestId('stack-view')).toHaveTextContent(StackView.FULL); - }); - - it('sets stack type to minified when option added', async () => { - render( - - - ctx.setDisplayOptions(['minified'])}> - Add Minified - - - ); - - expect(screen.getByTestId('stack-type')).toHaveTextContent(StackType.ORIGINAL); - - await userEvent.click(screen.getByText('Add Minified')); - - expect(screen.getByTestId('stack-type')).toHaveTextContent(StackType.MINIFIED); - }); - - it('sets stack view to RAW when option added', async () => { - render( - - - ctx.setDisplayOptions(['raw-stack-trace'])}> - Add Raw View - - - ); - - await userEvent.click(screen.getByText('Add Raw View')); - - expect(screen.getByTestId('stack-view')).toHaveTextContent(StackView.RAW); - }); -}); diff --git a/static/app/components/events/interfaces/stackTraceContext.tsx b/static/app/components/events/interfaces/stackTraceContext.tsx deleted file mode 100644 index c0f3e75b6641..000000000000 --- a/static/app/components/events/interfaces/stackTraceContext.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import {createContext, useContext, useMemo, useState} from 'react'; - -import {StackType, StackView} from 'sentry/types/stacktrace'; -import {defined} from 'sentry/utils/defined'; -import {useDetailedProject} from 'sentry/utils/project/useDetailedProject'; -import {useLocalStorageState} from 'sentry/utils/useLocalStorageState'; -import {useOrganization} from 'sentry/utils/useOrganization'; - -type DisplayOptions = - | 'absolute-addresses' - | 'absolute-file-paths' - | 'minified' - | 'raw-stack-trace' - | 'verbose-function-names'; - -interface StackTraceContextOptions { - children: React.ReactNode; - /** - * Whether the stack trace has system frames. - * Removes the minified option if false. - */ - hasSystemFrames: boolean; - projectSlug: string; - /** - * Override the default newest frames first - * @default true - */ - defaultIsNewestFramesFirst?: boolean; - /** - * Override any options and force the stack trace to be full - * @default false - */ - forceFullStackTrace?: boolean; -} - -interface StacktraceContextType { - /** - * Display options for the stack trace - */ - displayOptions: DisplayOptions[]; - /** - * Prefer using the stackView prop instead. - * This should only be used to control the full/relevant toggle. - */ - forceFullStackTrace: boolean; - /** - * Whether the project has SCM source context enabled - */ - hasScmSourceContext: boolean; - /** - * Display full stack trace or filter to relevant frames. - * This should only be used to control the full/relevant toggle. - * @default false - */ - isFullStackTrace: boolean; - /** - * Sort frames by either recent first or recent last - * @default true - */ - isNewestFramesFirst: boolean; - /** - * Update display options - */ - setDisplayOptions: React.Dispatch>; - /** - * Toggle the filter to either relevant or full stack trace - */ - setIsFullStackTrace: React.Dispatch>; - /** - * Toggle the sort to either recent first or recent last - */ - setIsNewestFramesFirst: React.Dispatch>; - /** - * The type of stack trace to display - */ - stackType: StackType; - /** - * Derrived from display options - */ - stackView: StackView; -} - -const IssueStacktraceContext = createContext({ - stackView: StackView.APP, - stackType: StackType.ORIGINAL, - displayOptions: [], - setDisplayOptions: () => {}, - hasScmSourceContext: false, - isFullStackTrace: true, - forceFullStackTrace: false, - setIsFullStackTrace: () => {}, - isNewestFramesFirst: true, - setIsNewestFramesFirst: () => {}, -}); - -export function StacktraceContext({ - children, - projectSlug, - hasSystemFrames, - forceFullStackTrace = false, - defaultIsNewestFramesFirst = true, -}: StackTraceContextOptions) { - const organization = useOrganization(); - const {data: detailedProject} = useDetailedProject( - {orgSlug: organization.slug, projectSlug: projectSlug ?? ''}, - {enabled: defined(projectSlug)} - ); - const hasScmSourceContext = !!detailedProject?.scmSourceContextEnabled; - - const [isFullStackTrace, setIsFullStackTrace] = useState(false); - const [isNewestFramesFirst, setIsNewestFramesFirst] = useState( - defaultIsNewestFramesFirst - ); - - const [displayOptions, setDisplayOptions] = useLocalStorageState( - `issue-details-stracktrace-display-${organization.slug}-${projectSlug}`, - [] - ); - - const stackView = displayOptions.includes('raw-stack-trace') - ? StackView.RAW - : isFullStackTrace || forceFullStackTrace - ? StackView.FULL - : StackView.APP; - - const stackType = - hasSystemFrames && displayOptions.includes('minified') - ? StackType.MINIFIED - : StackType.ORIGINAL; - - const value = useMemo( - (): StacktraceContextType => ({ - isFullStackTrace: isFullStackTrace || forceFullStackTrace, - setIsFullStackTrace, - isNewestFramesFirst, - setIsNewestFramesFirst, - displayOptions, - setDisplayOptions, - hasScmSourceContext, - stackView, - stackType, - forceFullStackTrace, - }), - [ - isFullStackTrace, - forceFullStackTrace, - isNewestFramesFirst, - displayOptions, - setDisplayOptions, - hasScmSourceContext, - stackView, - stackType, - ] - ); - - return ( - - {children} - - ); -} - -export function useStacktraceContext() { - return useContext(IssueStacktraceContext); -} diff --git a/static/app/components/events/interfaces/template.spec.tsx b/static/app/components/events/interfaces/template.spec.tsx new file mode 100644 index 000000000000..21ebbec229ce --- /dev/null +++ b/static/app/components/events/interfaces/template.spec.tsx @@ -0,0 +1,29 @@ +import {EventFixture} from 'sentry-fixture/event'; +import {FrameFixture} from 'sentry-fixture/frame'; + +import {render, screen} from 'sentry-test/reactTestingLibrary'; + +import {Template} from 'sentry/components/events/interfaces/template'; +import {EntryType} from 'sentry/types/event'; + +it('renders template source context expanded in the new stack trace', () => { + const frame = FrameFixture({ + platform: 'python', + filename: 'template.html', + context: [[3, '{{ example.value }}']], + lineNo: 3, + vars: {example: 'value'}, + }); + render( +