stackTracePreview.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  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. {query: {referrer: 'api.issues.preview-error'}}
  130. );
  131. setEvent(evt);
  132. setStatus('loaded');
  133. } catch {
  134. setEvent(null);
  135. setStatus('error');
  136. } finally {
  137. onRequestEnd();
  138. }
  139. }, [
  140. event,
  141. issueId,
  142. eventId,
  143. projectSlug,
  144. onRequestEnd,
  145. onRequestBegin,
  146. api,
  147. organization.slug,
  148. ]);
  149. useEffect(() => {
  150. fetchData();
  151. return () => {
  152. onUnmount();
  153. };
  154. }, [fetchData, onUnmount]);
  155. const stacktrace = useMemo(() => (event ? getStacktrace(event) : null), [event]);
  156. switch (status) {
  157. case 'loading':
  158. return (
  159. <NoStackTraceWrapper>
  160. <LoadingIndicator hideMessage size={32} />
  161. </NoStackTraceWrapper>
  162. );
  163. case 'error':
  164. return (
  165. <NoStackTraceWrapper>{t('Failed to load stack trace.')}</NoStackTraceWrapper>
  166. );
  167. case 'loaded': {
  168. if (stacktrace && event) {
  169. return (
  170. <StackTracePreviewWrapper>
  171. <StackTracePreviewContent
  172. event={event}
  173. stacktrace={stacktrace}
  174. groupingCurrentLevel={groupingCurrentLevel}
  175. orgFeatures={organization.features}
  176. />
  177. </StackTracePreviewWrapper>
  178. );
  179. }
  180. return (
  181. <NoStackTraceWrapper>
  182. {t('There is no stack trace available for this issue.')}
  183. </NoStackTraceWrapper>
  184. );
  185. }
  186. default: {
  187. return null;
  188. }
  189. }
  190. }
  191. function StackTracePreview({children, ...props}: StackTracePreviewProps) {
  192. const organization = useOrganization();
  193. const {shouldShowLoadingState, onRequestBegin, onRequestEnd, reset} =
  194. useDelayedLoadingState();
  195. const hasGroupingStacktraceUI = organization.features.includes(
  196. 'grouping-stacktrace-ui'
  197. );
  198. return (
  199. <Wrapper
  200. data-testid="stacktrace-preview"
  201. hasGroupingStacktraceUI={hasGroupingStacktraceUI}
  202. >
  203. <GroupPreviewHovercard
  204. hide={!shouldShowLoadingState}
  205. body={
  206. <StackTracePreviewBody
  207. onRequestBegin={onRequestBegin}
  208. onRequestEnd={onRequestEnd}
  209. onUnmount={reset}
  210. {...props}
  211. />
  212. }
  213. >
  214. {children}
  215. </GroupPreviewHovercard>
  216. </Wrapper>
  217. );
  218. }
  219. export {StackTracePreview};
  220. const Wrapper = styled('span')<{
  221. hasGroupingStacktraceUI: boolean;
  222. }>`
  223. ${p =>
  224. p.hasGroupingStacktraceUI &&
  225. css`
  226. display: inline-flex;
  227. overflow: hidden;
  228. height: 100%;
  229. > span:first-child {
  230. ${p.theme.overflowEllipsis}
  231. }
  232. `}
  233. `;
  234. const StackTracePreviewWrapper = styled('div')`
  235. width: 700px;
  236. .traceback {
  237. margin-bottom: 0;
  238. border: 0;
  239. box-shadow: none;
  240. }
  241. `;
  242. const NoStackTraceWrapper = styled('div')`
  243. color: ${p => p.theme.subText};
  244. padding: ${space(1.5)};
  245. font-size: ${p => p.theme.fontSizeMedium};
  246. display: flex;
  247. align-items: center;
  248. justify-content: center;
  249. min-height: 56px;
  250. `;