stackTracePreview.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import {useCallback, useEffect, useMemo, useState} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import StackTraceContent from 'sentry/components/events/interfaces/crashContent/stackTrace/content';
  5. import StackTraceContentV2 from 'sentry/components/events/interfaces/crashContent/stackTrace/contentV2';
  6. import StackTraceContentV3 from 'sentry/components/events/interfaces/crashContent/stackTrace/contentV3';
  7. import findBestThread from 'sentry/components/events/interfaces/threads/threadSelector/findBestThread';
  8. import getThreadStacktrace from 'sentry/components/events/interfaces/threads/threadSelector/getThreadStacktrace';
  9. import {isStacktraceNewestFirst} from 'sentry/components/events/interfaces/utils';
  10. import {GroupPreviewHovercard} from 'sentry/components/groupPreviewTooltip/groupPreviewHovercard';
  11. import {useDelayedLoadingState} from 'sentry/components/groupPreviewTooltip/utils';
  12. import LoadingIndicator from 'sentry/components/loadingIndicator';
  13. import {t} from 'sentry/locale';
  14. import space from 'sentry/styles/space';
  15. import {PlatformType} from 'sentry/types';
  16. import {EntryType, Event} from 'sentry/types/event';
  17. import {StacktraceType} from 'sentry/types/stacktrace';
  18. import {defined} from 'sentry/utils';
  19. import {isNativePlatform} from 'sentry/utils/platform';
  20. import useApi from 'sentry/utils/useApi';
  21. import useOrganization from 'sentry/utils/useOrganization';
  22. function getStacktrace(event: Event): StacktraceType | null {
  23. const exceptionsWithStacktrace =
  24. event.entries
  25. .find(e => e.type === EntryType.EXCEPTION)
  26. ?.data?.values.filter(({stacktrace}) => defined(stacktrace)) ?? [];
  27. const exceptionStacktrace: StacktraceType | undefined = isStacktraceNewestFirst()
  28. ? exceptionsWithStacktrace[exceptionsWithStacktrace.length - 1]?.stacktrace
  29. : exceptionsWithStacktrace[0]?.stacktrace;
  30. if (exceptionStacktrace) {
  31. return exceptionStacktrace;
  32. }
  33. const threads =
  34. event.entries.find(e => e.type === EntryType.THREADS)?.data?.values ?? [];
  35. const bestThread = findBestThread(threads);
  36. if (!bestThread) {
  37. return null;
  38. }
  39. const bestThreadStacktrace = getThreadStacktrace(false, bestThread);
  40. if (bestThreadStacktrace) {
  41. return bestThreadStacktrace;
  42. }
  43. return null;
  44. }
  45. function StackTracePreviewContent({
  46. event,
  47. stacktrace,
  48. orgFeatures = [],
  49. groupingCurrentLevel,
  50. }: {
  51. event: Event;
  52. stacktrace: StacktraceType;
  53. groupingCurrentLevel?: number;
  54. orgFeatures?: string[];
  55. }) {
  56. const includeSystemFrames = useMemo(() => {
  57. return stacktrace?.frames?.every(frame => !frame.inApp) ?? false;
  58. }, [stacktrace]);
  59. const framePlatform = stacktrace?.frames?.find(frame => !!frame.platform)?.platform;
  60. const platform = (framePlatform ?? event.platform ?? 'other') as PlatformType;
  61. const newestFirst = isStacktraceNewestFirst();
  62. const commonProps = {
  63. data: stacktrace,
  64. expandFirstFrame: false,
  65. includeSystemFrames,
  66. platform,
  67. newestFirst,
  68. event,
  69. isHoverPreviewed: true,
  70. };
  71. if (orgFeatures.includes('native-stack-trace-v2') && isNativePlatform(platform)) {
  72. return (
  73. <StackTraceContentV3 {...commonProps} groupingCurrentLevel={groupingCurrentLevel} />
  74. );
  75. }
  76. if (orgFeatures.includes('grouping-stacktrace-ui')) {
  77. return (
  78. <StackTraceContentV2 {...commonProps} groupingCurrentLevel={groupingCurrentLevel} />
  79. );
  80. }
  81. return <StackTraceContent {...commonProps} />;
  82. }
  83. type StackTracePreviewProps = {
  84. children: React.ReactChild;
  85. issueId: string;
  86. eventId?: string;
  87. groupingCurrentLevel?: number;
  88. projectSlug?: string;
  89. };
  90. interface StackTracePreviewBodyProps
  91. extends Pick<
  92. StackTracePreviewProps,
  93. 'issueId' | 'eventId' | 'groupingCurrentLevel' | 'projectSlug'
  94. > {
  95. onRequestBegin: () => void;
  96. onRequestEnd: () => void;
  97. onUnmount: () => void;
  98. }
  99. function StackTracePreviewBody({
  100. issueId,
  101. eventId,
  102. groupingCurrentLevel,
  103. projectSlug,
  104. onRequestBegin,
  105. onRequestEnd,
  106. onUnmount,
  107. }: StackTracePreviewBodyProps) {
  108. const api = useApi();
  109. const organization = useOrganization();
  110. const [status, setStatus] = useState<'loading' | 'loaded' | 'error'>('loading');
  111. const [event, setEvent] = useState<Event | null>(null);
  112. const fetchData = useCallback(async () => {
  113. onRequestBegin();
  114. // Data is already loaded
  115. if (event) {
  116. onRequestEnd();
  117. return;
  118. }
  119. // These are required props to load data
  120. if (issueId && eventId && !projectSlug) {
  121. onRequestEnd();
  122. return;
  123. }
  124. try {
  125. const evt = await api.requestPromise(
  126. eventId && projectSlug
  127. ? `/projects/${organization.slug}/${projectSlug}/events/${eventId}/`
  128. : `/issues/${issueId}/events/latest/?collapse=stacktraceOnly`
  129. );
  130. setEvent(evt);
  131. setStatus('loaded');
  132. } catch {
  133. setEvent(null);
  134. setStatus('error');
  135. } finally {
  136. onRequestEnd();
  137. }
  138. }, [
  139. event,
  140. issueId,
  141. eventId,
  142. projectSlug,
  143. onRequestEnd,
  144. onRequestBegin,
  145. api,
  146. organization.slug,
  147. ]);
  148. useEffect(() => {
  149. fetchData();
  150. return () => {
  151. onUnmount();
  152. };
  153. }, [fetchData, onUnmount]);
  154. const stacktrace = useMemo(() => (event ? getStacktrace(event) : null), [event]);
  155. switch (status) {
  156. case 'loading':
  157. return (
  158. <NoStackTraceWrapper>
  159. <LoadingIndicator hideMessage size={32} />
  160. </NoStackTraceWrapper>
  161. );
  162. case 'error':
  163. return (
  164. <NoStackTraceWrapper>{t('Failed to load stack trace.')}</NoStackTraceWrapper>
  165. );
  166. case 'loaded': {
  167. if (stacktrace && event) {
  168. return (
  169. <StackTracePreviewWrapper>
  170. <StackTracePreviewContent
  171. event={event}
  172. stacktrace={stacktrace}
  173. groupingCurrentLevel={groupingCurrentLevel}
  174. orgFeatures={organization.features}
  175. />
  176. </StackTracePreviewWrapper>
  177. );
  178. }
  179. return (
  180. <NoStackTraceWrapper>
  181. {t('There is no stack trace available for this issue.')}
  182. </NoStackTraceWrapper>
  183. );
  184. }
  185. default: {
  186. return null;
  187. }
  188. }
  189. }
  190. function StackTracePreview({children, ...props}: StackTracePreviewProps) {
  191. const organization = useOrganization();
  192. const {shouldShowLoadingState, onRequestBegin, onRequestEnd, reset} =
  193. useDelayedLoadingState();
  194. const hasGroupingStacktraceUI = organization.features.includes(
  195. 'grouping-stacktrace-ui'
  196. );
  197. return (
  198. <Wrapper
  199. data-testid="stacktrace-preview"
  200. hasGroupingStacktraceUI={hasGroupingStacktraceUI}
  201. >
  202. <GroupPreviewHovercard
  203. hide={!shouldShowLoadingState}
  204. body={
  205. <StackTracePreviewBody
  206. onRequestBegin={onRequestBegin}
  207. onRequestEnd={onRequestEnd}
  208. onUnmount={reset}
  209. {...props}
  210. />
  211. }
  212. >
  213. {children}
  214. </GroupPreviewHovercard>
  215. </Wrapper>
  216. );
  217. }
  218. export {StackTracePreview};
  219. const Wrapper = styled('span')<{
  220. hasGroupingStacktraceUI: boolean;
  221. }>`
  222. ${p =>
  223. p.hasGroupingStacktraceUI &&
  224. css`
  225. display: inline-flex;
  226. overflow: hidden;
  227. height: 100%;
  228. > span:first-child {
  229. ${p.theme.overflowEllipsis}
  230. }
  231. `}
  232. `;
  233. const StackTracePreviewWrapper = styled('div')`
  234. width: 700px;
  235. .traceback {
  236. margin-bottom: 0;
  237. border: 0;
  238. box-shadow: none;
  239. }
  240. `;
  241. const NoStackTraceWrapper = styled('div')`
  242. color: ${p => p.theme.subText};
  243. padding: ${space(1.5)};
  244. font-size: ${p => p.theme.fontSizeMedium};
  245. display: flex;
  246. align-items: center;
  247. justify-content: center;
  248. min-height: 56px;
  249. `;