threads.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. import {Fragment, useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import ErrorBoundary from 'sentry/components/errorBoundary';
  4. import {EventDataSection} from 'sentry/components/events/eventDataSection';
  5. import {StacktraceBanners} from 'sentry/components/events/interfaces/crashContent/exception/banners/stacktraceBanners';
  6. import {getLockReason} from 'sentry/components/events/interfaces/threads/threadSelector/lockReason';
  7. import {
  8. getMappedThreadState,
  9. getThreadStateHelpText,
  10. ThreadStates,
  11. } from 'sentry/components/events/interfaces/threads/threadSelector/threadStates';
  12. import Pill from 'sentry/components/pill';
  13. import Pills from 'sentry/components/pills';
  14. import QuestionTooltip from 'sentry/components/questionTooltip';
  15. import TextOverflow from 'sentry/components/textOverflow';
  16. import {IconClock, IconInfo, IconLock, IconPlay, IconTimer} from 'sentry/icons';
  17. import {t} from 'sentry/locale';
  18. import {space} from 'sentry/styles/space';
  19. import type {Event, Organization, Project, Thread} from 'sentry/types';
  20. import {EntryType, StackType, StackView} from 'sentry/types';
  21. import {defined} from 'sentry/utils';
  22. import {PermalinkTitle, TraceEventDataSection} from '../traceEventDataSection';
  23. import {ExceptionContent} from './crashContent/exception';
  24. import {StackTraceContent} from './crashContent/stackTrace';
  25. import ThreadSelector from './threads/threadSelector';
  26. import findBestThread from './threads/threadSelector/findBestThread';
  27. import getThreadException from './threads/threadSelector/getThreadException';
  28. import getThreadStacktrace from './threads/threadSelector/getThreadStacktrace';
  29. import NoStackTraceMessage from './noStackTraceMessage';
  30. import {inferPlatform, isStacktraceNewestFirst} from './utils';
  31. type ExceptionProps = React.ComponentProps<typeof ExceptionContent>;
  32. type Props = Pick<ExceptionProps, 'groupingCurrentLevel' | 'hasHierarchicalGrouping'> & {
  33. data: {
  34. values?: Array<Thread>;
  35. };
  36. event: Event;
  37. organization: Organization;
  38. projectSlug: Project['slug'];
  39. };
  40. function getIntendedStackView(
  41. thread: Thread,
  42. exception: ReturnType<typeof getThreadException>
  43. ): StackView {
  44. if (exception) {
  45. return exception.values.find(value => !!value.stacktrace?.hasSystemFrames)
  46. ? StackView.APP
  47. : StackView.FULL;
  48. }
  49. const stacktrace = getThreadStacktrace(false, thread);
  50. return stacktrace?.hasSystemFrames ? StackView.APP : StackView.FULL;
  51. }
  52. export function getThreadStateIcon(state: ThreadStates | undefined) {
  53. if (state === null || state === undefined) {
  54. return null;
  55. }
  56. switch (state) {
  57. case ThreadStates.BLOCKED:
  58. return <IconLock isSolid />;
  59. case ThreadStates.TIMED_WAITING:
  60. return <IconTimer />;
  61. case ThreadStates.WAITING:
  62. return <IconClock />;
  63. case ThreadStates.RUNNABLE:
  64. return <IconPlay />;
  65. default:
  66. return <IconInfo />;
  67. }
  68. }
  69. // We want to set the active thread every time the event changes because the best thread might not be the same between events
  70. const useActiveThreadState = (
  71. event: Event,
  72. threads: Thread[]
  73. ): [Thread | undefined, (newState: Thread | undefined) => void] => {
  74. const bestThread = threads.length ? findBestThread(threads) : undefined;
  75. const [activeThread, setActiveThread] = useState<Thread | undefined>(() => bestThread);
  76. useEffect(() => {
  77. setActiveThread(bestThread);
  78. // eslint-disable-next-line react-hooks/exhaustive-deps
  79. }, [event.id]);
  80. return [activeThread, setActiveThread];
  81. };
  82. export function Threads({
  83. data,
  84. event,
  85. projectSlug,
  86. hasHierarchicalGrouping,
  87. groupingCurrentLevel,
  88. organization,
  89. }: Props) {
  90. const threads = data.values ?? [];
  91. const [activeThread, setActiveThread] = useActiveThreadState(event, threads);
  92. const stackTraceNotFound = !threads.length;
  93. const hasMoreThanOneThread = threads.length > 1;
  94. const exception = getThreadException(event, activeThread);
  95. const entryIndex = exception
  96. ? event.entries.findIndex(entry => entry.type === EntryType.EXCEPTION)
  97. : event.entries.findIndex(entry => entry.type === EntryType.THREADS);
  98. const meta = event._meta?.entries?.[entryIndex]?.data?.values;
  99. const stackView = activeThread
  100. ? getIntendedStackView(activeThread, exception)
  101. : undefined;
  102. function renderPills() {
  103. const {
  104. id,
  105. name,
  106. current,
  107. crashed,
  108. state: threadState,
  109. heldLocks,
  110. } = activeThread ?? {};
  111. if (id === null || id === undefined || !name) {
  112. return null;
  113. }
  114. const threadStateDisplay = getMappedThreadState(threadState);
  115. const lockReason = getLockReason(heldLocks);
  116. return (
  117. <Pills>
  118. <Pill name={t('id')} value={id} />
  119. {!!name?.trim() && <Pill name={t('name')} value={name} />}
  120. {current !== undefined && <Pill name={t('was active')} value={current} />}
  121. {crashed !== undefined && (
  122. <Pill name={t('errored')} className={crashed ? 'false' : 'true'}>
  123. {crashed ? t('yes') : t('no')}
  124. </Pill>
  125. )}
  126. {threadStateDisplay !== undefined && (
  127. <Pill name={t('state')} value={threadStateDisplay} />
  128. )}
  129. {defined(lockReason) && <Pill name={t('lock reason')} value={lockReason} />}
  130. </Pills>
  131. );
  132. }
  133. function renderContent({
  134. display,
  135. recentFirst,
  136. fullStackTrace,
  137. }: Parameters<React.ComponentProps<typeof TraceEventDataSection>['children']>[0]) {
  138. const stackType = display.includes('minified')
  139. ? StackType.MINIFIED
  140. : StackType.ORIGINAL;
  141. if (exception) {
  142. return (
  143. <ExceptionContent
  144. stackType={stackType}
  145. stackView={
  146. display.includes('raw-stack-trace')
  147. ? StackView.RAW
  148. : fullStackTrace
  149. ? StackView.FULL
  150. : StackView.APP
  151. }
  152. projectSlug={projectSlug}
  153. newestFirst={recentFirst}
  154. event={event}
  155. values={exception.values}
  156. groupingCurrentLevel={groupingCurrentLevel}
  157. hasHierarchicalGrouping={hasHierarchicalGrouping}
  158. meta={meta}
  159. threadId={activeThread?.id}
  160. />
  161. );
  162. }
  163. const stackTrace = getThreadStacktrace(
  164. stackType !== StackType.ORIGINAL,
  165. activeThread
  166. );
  167. if (stackTrace) {
  168. return (
  169. <StackTraceContent
  170. stacktrace={stackTrace}
  171. stackView={
  172. display.includes('raw-stack-trace')
  173. ? StackView.RAW
  174. : fullStackTrace
  175. ? StackView.FULL
  176. : StackView.APP
  177. }
  178. newestFirst={recentFirst}
  179. event={event}
  180. platform={platform}
  181. groupingCurrentLevel={groupingCurrentLevel}
  182. hasHierarchicalGrouping={hasHierarchicalGrouping}
  183. meta={meta}
  184. threadId={activeThread?.id}
  185. />
  186. );
  187. }
  188. return (
  189. <NoStackTraceMessage
  190. message={activeThread?.crashed ? t('Thread Errored') : undefined}
  191. />
  192. );
  193. }
  194. const platform = inferPlatform(event, activeThread);
  195. const threadStateDisplay = getMappedThreadState(activeThread?.state);
  196. const {id: activeThreadId, name: activeThreadName} = activeThread ?? {};
  197. const hideThreadTags = activeThreadId === undefined || !activeThreadName;
  198. return (
  199. <Fragment>
  200. {hasMoreThanOneThread && organization.features.includes('anr-improvements') && (
  201. <Fragment>
  202. <Grid>
  203. <EventDataSection type={EntryType.THREADS} title={t('Threads')}>
  204. {activeThread && (
  205. <Wrapper>
  206. <ThreadSelector
  207. threads={threads}
  208. activeThread={activeThread}
  209. event={event}
  210. onChange={thread => {
  211. setActiveThread(thread);
  212. }}
  213. exception={exception}
  214. />
  215. </Wrapper>
  216. )}
  217. </EventDataSection>
  218. {activeThread?.state && (
  219. <EventDataSection type={EntryType.THREAD_STATE} title={t('Thread State')}>
  220. <ThreadStateWrapper>
  221. {getThreadStateIcon(threadStateDisplay)}
  222. <ThreadState>{threadStateDisplay}</ThreadState>
  223. {threadStateDisplay && (
  224. <QuestionTooltip
  225. position="top"
  226. size="xs"
  227. containerDisplayMode="block"
  228. title={getThreadStateHelpText(threadStateDisplay)}
  229. />
  230. )}
  231. <LockReason>{getLockReason(activeThread?.heldLocks)}</LockReason>
  232. </ThreadStateWrapper>
  233. </EventDataSection>
  234. )}
  235. </Grid>
  236. {!hideThreadTags && (
  237. <EventDataSection type={EntryType.THREAD_TAGS} title={t('Thread Tags')}>
  238. {renderPills()}
  239. </EventDataSection>
  240. )}
  241. </Fragment>
  242. )}
  243. <TraceEventDataSection
  244. type={EntryType.THREADS}
  245. projectSlug={projectSlug}
  246. eventId={event.id}
  247. recentFirst={isStacktraceNewestFirst()}
  248. fullStackTrace={stackView === StackView.FULL}
  249. title={
  250. hasMoreThanOneThread &&
  251. activeThread &&
  252. !organization.features.includes('anr-improvements') ? (
  253. <ThreadSelector
  254. threads={threads}
  255. activeThread={activeThread}
  256. event={event}
  257. onChange={thread => {
  258. setActiveThread(thread);
  259. }}
  260. exception={exception}
  261. fullWidth
  262. />
  263. ) : (
  264. <PermalinkTitle>
  265. {hasMoreThanOneThread ? t('Thread Stack Trace') : t('Stack Trace')}
  266. </PermalinkTitle>
  267. )
  268. }
  269. platform={platform}
  270. hasMinified={
  271. !!exception?.values?.find(value => value.rawStacktrace) ||
  272. !!activeThread?.rawStacktrace
  273. }
  274. hasVerboseFunctionNames={
  275. !!exception?.values?.some(
  276. value =>
  277. !!value.stacktrace?.frames?.some(
  278. frame =>
  279. !!frame.rawFunction &&
  280. !!frame.function &&
  281. frame.rawFunction !== frame.function
  282. )
  283. ) ||
  284. !!activeThread?.stacktrace?.frames?.some(
  285. frame =>
  286. !!frame.rawFunction &&
  287. !!frame.function &&
  288. frame.rawFunction !== frame.function
  289. )
  290. }
  291. hasAbsoluteFilePaths={
  292. !!exception?.values?.some(
  293. value => !!value.stacktrace?.frames?.some(frame => !!frame.filename)
  294. ) || !!activeThread?.stacktrace?.frames?.some(frame => !!frame.filename)
  295. }
  296. hasAbsoluteAddresses={
  297. !!exception?.values?.some(
  298. value => !!value.stacktrace?.frames?.some(frame => !!frame.instructionAddr)
  299. ) || !!activeThread?.stacktrace?.frames?.some(frame => !!frame.instructionAddr)
  300. }
  301. hasAppOnlyFrames={
  302. !!exception?.values?.some(
  303. value => !!value.stacktrace?.frames?.some(frame => frame.inApp !== true)
  304. ) || !!activeThread?.stacktrace?.frames?.some(frame => frame.inApp !== true)
  305. }
  306. hasNewestFirst={
  307. !!exception?.values?.some(
  308. value => (value.stacktrace?.frames ?? []).length > 1
  309. ) || (activeThread?.stacktrace?.frames ?? []).length > 1
  310. }
  311. stackTraceNotFound={stackTraceNotFound}
  312. wrapTitle={false}
  313. >
  314. {childrenProps => {
  315. // TODO(scttcper): These are duplicated from renderContent, should consolidate
  316. const stackType = childrenProps.display.includes('minified')
  317. ? StackType.MINIFIED
  318. : StackType.ORIGINAL;
  319. const isRaw = childrenProps.display.includes('raw-stack-trace');
  320. const stackTrace = getThreadStacktrace(
  321. stackType !== StackType.ORIGINAL,
  322. activeThread
  323. );
  324. return (
  325. <Fragment>
  326. {!organization.features.includes('anr-improvements') && renderPills()}
  327. {stackTrace && !isRaw && (
  328. <ErrorBoundary customComponent={null}>
  329. <StacktraceBanners event={event} stacktrace={stackTrace} />
  330. </ErrorBoundary>
  331. )}
  332. {renderContent(childrenProps)}
  333. </Fragment>
  334. );
  335. }}
  336. </TraceEventDataSection>
  337. </Fragment>
  338. );
  339. }
  340. const Grid = styled('div')`
  341. display: grid;
  342. grid-template-columns: auto 1fr;
  343. `;
  344. const ThreadStateWrapper = styled('div')`
  345. display: flex;
  346. position: relative;
  347. flex-direction: row;
  348. align-items: flex-start;
  349. gap: ${space(0.5)};
  350. `;
  351. const ThreadState = styled(TextOverflow)`
  352. max-width: 100%;
  353. text-align: left;
  354. font-weight: bold;
  355. `;
  356. const LockReason = styled(TextOverflow)`
  357. font-weight: 400;
  358. color: ${p => p.theme.gray300};
  359. `;
  360. const Wrapper = styled('div')`
  361. align-items: center;
  362. flex-wrap: wrap;
  363. flex-grow: 1;
  364. justify-content: flex-start;
  365. `;