threads.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. import {Fragment, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Button} from 'sentry/components/button';
  4. import ButtonBar from 'sentry/components/buttonBar';
  5. import {CommitRow} from 'sentry/components/commitRow';
  6. import {Flex} from 'sentry/components/container/flex';
  7. import ErrorBoundary from 'sentry/components/errorBoundary';
  8. import {getLockReason} from 'sentry/components/events/interfaces/threads/threadSelector/lockReason';
  9. import {
  10. getMappedThreadState,
  11. getThreadStateHelpText,
  12. ThreadStates,
  13. } from 'sentry/components/events/interfaces/threads/threadSelector/threadStates';
  14. import {SuspectCommits} from 'sentry/components/events/suspectCommits';
  15. import Pill from 'sentry/components/pill';
  16. import Pills from 'sentry/components/pills';
  17. import QuestionTooltip from 'sentry/components/questionTooltip';
  18. import TextOverflow from 'sentry/components/textOverflow';
  19. import {
  20. IconChevron,
  21. IconClock,
  22. IconInfo,
  23. IconLock,
  24. IconPlay,
  25. IconTimer,
  26. } from 'sentry/icons';
  27. import {t, tn} from 'sentry/locale';
  28. import {space} from 'sentry/styles/space';
  29. import type {Event, Thread} from 'sentry/types/event';
  30. import {EntryType} from 'sentry/types/event';
  31. import type {Group} from 'sentry/types/group';
  32. import type {Project} from 'sentry/types/project';
  33. import {StackType, StackView} from 'sentry/types/stacktrace';
  34. import {defined} from 'sentry/utils';
  35. import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
  36. import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
  37. import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
  38. import {TraceEventDataSection} from '../traceEventDataSection';
  39. import {ExceptionContent} from './crashContent/exception';
  40. import {StackTraceContent} from './crashContent/stackTrace';
  41. import ThreadSelector from './threads/threadSelector';
  42. import findBestThread from './threads/threadSelector/findBestThread';
  43. import getThreadException from './threads/threadSelector/getThreadException';
  44. import getThreadStacktrace from './threads/threadSelector/getThreadStacktrace';
  45. import NoStackTraceMessage from './noStackTraceMessage';
  46. import {inferPlatform, isStacktraceNewestFirst} from './utils';
  47. type ExceptionProps = React.ComponentProps<typeof ExceptionContent>;
  48. type Props = Pick<ExceptionProps, 'groupingCurrentLevel'> & {
  49. data: {
  50. values?: Array<Thread>;
  51. };
  52. event: Event;
  53. group: Group | undefined;
  54. projectSlug: Project['slug'];
  55. };
  56. function getIntendedStackView(
  57. thread: Thread,
  58. exception: ReturnType<typeof getThreadException>
  59. ): StackView {
  60. if (exception) {
  61. return exception.values.find(value => !!value.stacktrace?.hasSystemFrames)
  62. ? StackView.APP
  63. : StackView.FULL;
  64. }
  65. const stacktrace = getThreadStacktrace(false, thread);
  66. return stacktrace?.hasSystemFrames ? StackView.APP : StackView.FULL;
  67. }
  68. function ThreadStateIcon({state}: {state: ThreadStates | undefined}) {
  69. if (state === null || state === undefined) {
  70. return null;
  71. }
  72. switch (state) {
  73. case ThreadStates.BLOCKED:
  74. return <IconLock locked />;
  75. case ThreadStates.TIMED_WAITING:
  76. return <IconTimer />;
  77. case ThreadStates.WAITING:
  78. return <IconClock />;
  79. case ThreadStates.RUNNABLE:
  80. return <IconPlay />;
  81. default:
  82. return <IconInfo />;
  83. }
  84. }
  85. // We want to set the active thread every time the event changes because the best thread might not be the same between events
  86. const useActiveThreadState = (
  87. event: Event,
  88. threads: Thread[]
  89. ): [Thread | undefined, (newState: Thread | undefined) => void] => {
  90. const bestThread = threads.length ? findBestThread(threads) : undefined;
  91. const [activeThread, setActiveThread] = useState<Thread | undefined>(() => bestThread);
  92. useEffect(() => {
  93. setActiveThread(bestThread);
  94. // eslint-disable-next-line react-hooks/exhaustive-deps
  95. }, [event.id]);
  96. return [activeThread, setActiveThread];
  97. };
  98. export function Threads({data, event, projectSlug, groupingCurrentLevel, group}: Props) {
  99. // Sort threads by crashed first
  100. const threads = useMemo(
  101. () => (data.values ?? []).toSorted((a, b) => Number(b.crashed) - Number(a.crashed)),
  102. [data.values]
  103. );
  104. const hasStreamlinedUI = useHasStreamlinedUI();
  105. const [activeThread, setActiveThread] = useActiveThreadState(event, threads);
  106. const stackTraceNotFound = !threads.length;
  107. const hasMoreThanOneThread = threads.length > 1;
  108. const exception = useMemo(
  109. () => getThreadException(event, activeThread),
  110. [event, activeThread]
  111. );
  112. const entryIndex = exception
  113. ? event.entries.findIndex(entry => entry.type === EntryType.EXCEPTION)
  114. : event.entries.findIndex(entry => entry.type === EntryType.THREADS);
  115. const meta = event._meta?.entries?.[entryIndex]?.data?.values;
  116. const stackView = activeThread
  117. ? getIntendedStackView(activeThread, exception)
  118. : undefined;
  119. function renderPills() {
  120. const {
  121. id,
  122. name,
  123. current,
  124. crashed,
  125. state: threadState,
  126. heldLocks,
  127. } = activeThread ?? {};
  128. if (id === null || id === undefined || !name) {
  129. return null;
  130. }
  131. const threadStateDisplay = getMappedThreadState(threadState);
  132. const lockReason = getLockReason(heldLocks);
  133. return (
  134. <Pills>
  135. <Pill name={t('id')} value={id} />
  136. {!!name?.trim() && <Pill name={t('name')} value={name} />}
  137. {current !== undefined && <Pill name={t('was active')} value={current} />}
  138. {crashed !== undefined && (
  139. <Pill name={t('errored')} className={crashed ? 'false' : 'true'}>
  140. {crashed ? t('yes') : t('no')}
  141. </Pill>
  142. )}
  143. {threadStateDisplay !== undefined && (
  144. <Pill name={t('state')} value={threadStateDisplay} />
  145. )}
  146. {defined(lockReason) && <Pill name={t('lock reason')} value={lockReason} />}
  147. </Pills>
  148. );
  149. }
  150. function renderContent({
  151. display,
  152. recentFirst,
  153. fullStackTrace,
  154. }: Parameters<React.ComponentProps<typeof TraceEventDataSection>['children']>[0]) {
  155. const stackType = display.includes('minified')
  156. ? StackType.MINIFIED
  157. : StackType.ORIGINAL;
  158. if (exception) {
  159. return (
  160. <ExceptionContent
  161. stackType={stackType}
  162. stackView={
  163. display.includes('raw-stack-trace')
  164. ? StackView.RAW
  165. : fullStackTrace
  166. ? StackView.FULL
  167. : StackView.APP
  168. }
  169. projectSlug={projectSlug}
  170. newestFirst={recentFirst}
  171. event={event}
  172. values={exception.values}
  173. groupingCurrentLevel={groupingCurrentLevel}
  174. meta={meta}
  175. threadId={activeThread?.id}
  176. />
  177. );
  178. }
  179. const stackTrace = getThreadStacktrace(
  180. stackType !== StackType.ORIGINAL,
  181. activeThread
  182. );
  183. if (stackTrace) {
  184. return (
  185. <StackTraceContent
  186. stacktrace={stackTrace}
  187. stackView={
  188. display.includes('raw-stack-trace')
  189. ? StackView.RAW
  190. : fullStackTrace
  191. ? StackView.FULL
  192. : StackView.APP
  193. }
  194. newestFirst={recentFirst}
  195. event={event}
  196. platform={platform}
  197. groupingCurrentLevel={groupingCurrentLevel}
  198. meta={meta}
  199. threadId={activeThread?.id}
  200. />
  201. );
  202. }
  203. return (
  204. <NoStackTraceMessage
  205. message={activeThread?.crashed ? t('Thread Errored') : undefined}
  206. />
  207. );
  208. }
  209. const platform = inferPlatform(event, activeThread);
  210. const threadStateDisplay = getMappedThreadState(activeThread?.state);
  211. const {id: activeThreadId, name: activeThreadName} = activeThread ?? {};
  212. const hideThreadTags = activeThreadId === undefined || !activeThreadName;
  213. function handleChangeThread(direction: 'previous' | 'next') {
  214. const currentIndex = threads.findIndex(thread => thread.id === activeThreadId);
  215. let nextIndex = direction === 'previous' ? currentIndex - 1 : currentIndex + 1;
  216. if (nextIndex < 0) {
  217. nextIndex = threads.length - 1;
  218. } else if (nextIndex >= threads.length) {
  219. nextIndex = 0;
  220. }
  221. setActiveThread(threads[nextIndex]);
  222. }
  223. const threadComponent = (
  224. <Fragment>
  225. {hasMoreThanOneThread && (
  226. <Fragment>
  227. <Grid>
  228. <div>
  229. <ThreadHeading>{t('Threads')}</ThreadHeading>
  230. {activeThread && (
  231. <Wrapper>
  232. <ButtonBar merged>
  233. <Button
  234. title={t('Previous Thread')}
  235. tooltipProps={{delay: 1000}}
  236. icon={<IconChevron direction="left" />}
  237. aria-label={t('Previous Thread')}
  238. size="xs"
  239. onClick={() => {
  240. handleChangeThread('previous');
  241. }}
  242. />
  243. <Button
  244. title={t('Next Thread')}
  245. tooltipProps={{delay: 1000}}
  246. icon={<IconChevron direction="right" />}
  247. aria-label={t('Next Thread')}
  248. size="xs"
  249. onClick={() => {
  250. handleChangeThread('next');
  251. }}
  252. />
  253. </ButtonBar>
  254. <ThreadSelector
  255. threads={threads}
  256. activeThread={activeThread}
  257. event={event}
  258. onChange={thread => {
  259. setActiveThread(thread);
  260. }}
  261. exception={exception}
  262. />
  263. </Wrapper>
  264. )}
  265. </div>
  266. {activeThread?.state && (
  267. <TheadStateContainer>
  268. <ThreadHeading>{t('Thread State')}</ThreadHeading>
  269. <ThreadStateWrapper>
  270. <ThreadStateIcon state={threadStateDisplay} />
  271. <TextOverflow>{threadStateDisplay}</TextOverflow>
  272. {threadStateDisplay && (
  273. <QuestionTooltip
  274. position="top"
  275. size="xs"
  276. containerDisplayMode="block"
  277. title={getThreadStateHelpText(threadStateDisplay)}
  278. skipWrapper
  279. />
  280. )}
  281. <LockReason>{getLockReason(activeThread?.heldLocks)}</LockReason>
  282. </ThreadStateWrapper>
  283. </TheadStateContainer>
  284. )}
  285. </Grid>
  286. {!hideThreadTags && (
  287. <div>
  288. <ThreadHeading>{t('Thread Tags')}</ThreadHeading>
  289. {renderPills()}
  290. </div>
  291. )}
  292. </Fragment>
  293. )}
  294. <TraceEventDataSection
  295. type={SectionKey.THREADS}
  296. projectSlug={projectSlug}
  297. eventId={event.id}
  298. recentFirst={isStacktraceNewestFirst()}
  299. fullStackTrace={stackView === StackView.FULL}
  300. title={hasMoreThanOneThread ? t('Thread Stack Trace') : t('Stack Trace')}
  301. platform={platform}
  302. isNestedSection={hasMoreThanOneThread}
  303. hasMinified={
  304. !!exception?.values?.find(value => value.rawStacktrace) ||
  305. !!activeThread?.rawStacktrace
  306. }
  307. hasVerboseFunctionNames={
  308. !!exception?.values?.some(
  309. value =>
  310. !!value.stacktrace?.frames?.some(
  311. frame =>
  312. !!frame.rawFunction &&
  313. !!frame.function &&
  314. frame.rawFunction !== frame.function
  315. )
  316. ) ||
  317. !!activeThread?.stacktrace?.frames?.some(
  318. frame =>
  319. !!frame.rawFunction &&
  320. !!frame.function &&
  321. frame.rawFunction !== frame.function
  322. )
  323. }
  324. hasAbsoluteFilePaths={
  325. !!exception?.values?.some(
  326. value => !!value.stacktrace?.frames?.some(frame => !!frame.filename)
  327. ) || !!activeThread?.stacktrace?.frames?.some(frame => !!frame.filename)
  328. }
  329. hasAbsoluteAddresses={
  330. !!exception?.values?.some(
  331. value => !!value.stacktrace?.frames?.some(frame => !!frame.instructionAddr)
  332. ) || !!activeThread?.stacktrace?.frames?.some(frame => !!frame.instructionAddr)
  333. }
  334. hasAppOnlyFrames={
  335. !!exception?.values?.some(
  336. value => !!value.stacktrace?.frames?.some(frame => frame.inApp !== true)
  337. ) || !!activeThread?.stacktrace?.frames?.some(frame => frame.inApp !== true)
  338. }
  339. hasNewestFirst={
  340. !!exception?.values?.some(
  341. value => (value.stacktrace?.frames ?? []).length > 1
  342. ) || (activeThread?.stacktrace?.frames ?? []).length > 1
  343. }
  344. stackTraceNotFound={stackTraceNotFound}
  345. >
  346. {childrenProps => {
  347. return (
  348. <Fragment>
  349. {renderContent(childrenProps)}
  350. {hasStreamlinedUI && group && (
  351. <ErrorBoundary
  352. mini
  353. message={t('There was an error loading the suspect commits')}
  354. >
  355. <SuspectCommits
  356. projectSlug={projectSlug}
  357. eventId={event.id}
  358. commitRow={CommitRow}
  359. group={group}
  360. />
  361. </ErrorBoundary>
  362. )}
  363. </Fragment>
  364. );
  365. }}
  366. </TraceEventDataSection>
  367. </Fragment>
  368. );
  369. if (hasStreamlinedUI) {
  370. // If there is only one thread, we expect the stacktrace to wrap itself in a section
  371. return hasMoreThanOneThread ? (
  372. <InterimSection
  373. title={tn('Stack Trace', 'Stack Traces', threads.length)}
  374. type={SectionKey.STACKTRACE}
  375. >
  376. <Flex column gap={space(2)}>
  377. {threadComponent}
  378. </Flex>
  379. </InterimSection>
  380. ) : (
  381. threadComponent
  382. );
  383. }
  384. return hasMoreThanOneThread ? (
  385. <ThreadTraceWrapper>{threadComponent}</ThreadTraceWrapper>
  386. ) : (
  387. threadComponent
  388. );
  389. }
  390. const Grid = styled('div')`
  391. display: grid;
  392. grid-template-columns: auto 1fr;
  393. gap: ${space(2)};
  394. `;
  395. const TheadStateContainer = styled('div')`
  396. ${p => p.theme.overflowEllipsis}
  397. `;
  398. const ThreadStateWrapper = styled('div')`
  399. display: flex;
  400. position: relative;
  401. flex-direction: row;
  402. align-items: center;
  403. gap: ${space(0.5)};
  404. `;
  405. const LockReason = styled(TextOverflow)`
  406. font-weight: ${p => p.theme.fontWeightNormal};
  407. color: ${p => p.theme.gray300};
  408. `;
  409. const Wrapper = styled('div')`
  410. display: flex;
  411. gap: ${space(1)};
  412. align-items: center;
  413. flex-wrap: wrap;
  414. flex-grow: 1;
  415. justify-content: flex-start;
  416. `;
  417. const ThreadTraceWrapper = styled('div')`
  418. display: flex;
  419. flex-direction: column;
  420. gap: ${space(2)};
  421. padding: ${space(1)} ${space(4)};
  422. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  423. padding: ${space(1)} ${space(2)};
  424. }
  425. `;
  426. const ThreadHeading = styled('h3')`
  427. color: ${p => p.theme.subText};
  428. font-size: ${p => p.theme.fontSizeMedium};
  429. font-weight: ${p => p.theme.fontWeightBold};
  430. margin-bottom: ${space(1)};
  431. `;