traceEventDataSection.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. import {
  2. AnchorHTMLAttributes,
  3. cloneElement,
  4. createContext,
  5. Fragment,
  6. useState,
  7. } from 'react';
  8. import styled from '@emotion/styled';
  9. import Button from 'sentry/components/button';
  10. import ButtonBar from 'sentry/components/buttonBar';
  11. import CompactSelect from 'sentry/components/compactSelect';
  12. import CompositeSelect from 'sentry/components/compositeSelect';
  13. import Tooltip from 'sentry/components/tooltip';
  14. import {IconEllipsis, IconLink, IconSort} from 'sentry/icons';
  15. import {t} from 'sentry/locale';
  16. import space from 'sentry/styles/space';
  17. import {PlatformType, Project} from 'sentry/types';
  18. import {Event} from 'sentry/types/event';
  19. import {STACK_TYPE} from 'sentry/types/stacktrace';
  20. import {isNativePlatform} from 'sentry/utils/platform';
  21. import useApi from 'sentry/utils/useApi';
  22. import useOrganization from 'sentry/utils/useOrganization';
  23. import EventDataSection from './eventDataSection';
  24. const sortByOptions = {
  25. 'recent-first': t('Newest'),
  26. 'recent-last': t('Oldest'),
  27. };
  28. export const displayOptions = {
  29. 'absolute-addresses': t('Absolute addresses'),
  30. 'absolute-file-paths': t('Absolute file paths'),
  31. minified: t('Unsymbolicated'),
  32. 'raw-stack-trace': t('Raw stack trace'),
  33. 'verbose-function-names': t('Verbose function names'),
  34. };
  35. type State = {
  36. display: Array<keyof typeof displayOptions>;
  37. fullStackTrace: boolean;
  38. sortBy: keyof typeof sortByOptions;
  39. };
  40. type ChildProps = Omit<State, 'sortBy'> & {recentFirst: boolean};
  41. type Props = {
  42. children: (childProps: ChildProps) => React.ReactNode;
  43. eventId: Event['id'];
  44. fullStackTrace: boolean;
  45. hasAbsoluteAddresses: boolean;
  46. hasAbsoluteFilePaths: boolean;
  47. hasAppOnlyFrames: boolean;
  48. hasMinified: boolean;
  49. hasNewestFirst: boolean;
  50. hasVerboseFunctionNames: boolean;
  51. platform: PlatformType;
  52. projectId: Project['id'];
  53. recentFirst: boolean;
  54. stackTraceNotFound: boolean;
  55. stackType: STACK_TYPE;
  56. title: React.ReactElement<any, any>;
  57. type: string;
  58. wrapTitle?: boolean;
  59. };
  60. export const TraceEventDataSectionContext = createContext<ChildProps | undefined>(
  61. undefined
  62. );
  63. export function TraceEventDataSection({
  64. type,
  65. title,
  66. wrapTitle,
  67. stackTraceNotFound,
  68. fullStackTrace,
  69. recentFirst,
  70. children,
  71. platform,
  72. stackType,
  73. projectId,
  74. eventId,
  75. hasNewestFirst,
  76. hasMinified,
  77. hasVerboseFunctionNames,
  78. hasAbsoluteFilePaths,
  79. hasAbsoluteAddresses,
  80. hasAppOnlyFrames,
  81. }: Props) {
  82. const api = useApi();
  83. const organization = useOrganization();
  84. const [state, setState] = useState<State>({
  85. sortBy: recentFirst ? 'recent-first' : 'recent-last',
  86. fullStackTrace: !hasAppOnlyFrames ? true : fullStackTrace,
  87. display: [],
  88. });
  89. function getDisplayOptions(): {
  90. label: string;
  91. value: keyof typeof displayOptions;
  92. disabled?: boolean;
  93. tooltip?: string;
  94. }[] {
  95. if (platform === 'objc' || platform === 'native' || platform === 'cocoa') {
  96. return [
  97. {
  98. label: displayOptions['absolute-addresses'],
  99. value: 'absolute-addresses',
  100. disabled: state.display.includes('raw-stack-trace') || !hasAbsoluteAddresses,
  101. tooltip: state.display.includes('raw-stack-trace')
  102. ? t('Not available on raw stack trace')
  103. : !hasAbsoluteAddresses
  104. ? t('Absolute addresses not available')
  105. : undefined,
  106. },
  107. {
  108. label: displayOptions['absolute-file-paths'],
  109. value: 'absolute-file-paths',
  110. disabled: state.display.includes('raw-stack-trace') || !hasAbsoluteFilePaths,
  111. tooltip: state.display.includes('raw-stack-trace')
  112. ? t('Not available on raw stack trace')
  113. : !hasAbsoluteFilePaths
  114. ? t('Absolute file paths not available')
  115. : undefined,
  116. },
  117. {
  118. label: displayOptions.minified,
  119. value: 'minified',
  120. disabled: !hasMinified,
  121. tooltip: !hasMinified ? t('Unsymbolicated version not available') : undefined,
  122. },
  123. {
  124. label: displayOptions['raw-stack-trace'],
  125. value: 'raw-stack-trace',
  126. },
  127. {
  128. label: displayOptions['verbose-function-names'],
  129. value: 'verbose-function-names',
  130. disabled: state.display.includes('raw-stack-trace') || !hasVerboseFunctionNames,
  131. tooltip: state.display.includes('raw-stack-trace')
  132. ? t('Not available on raw stack trace')
  133. : !hasVerboseFunctionNames
  134. ? t('Verbose function names not available')
  135. : undefined,
  136. },
  137. ];
  138. }
  139. if (platform.startsWith('python')) {
  140. return [
  141. {
  142. label: displayOptions['raw-stack-trace'],
  143. value: 'raw-stack-trace',
  144. },
  145. ];
  146. }
  147. return [
  148. {
  149. label: displayOptions.minified,
  150. value: 'minified',
  151. disabled: !hasMinified,
  152. tooltip: !hasMinified ? t('Minified version not available') : undefined,
  153. },
  154. {
  155. label: displayOptions['raw-stack-trace'],
  156. value: 'raw-stack-trace',
  157. },
  158. ];
  159. }
  160. const nativePlatform = isNativePlatform(platform);
  161. const minified = stackType === STACK_TYPE.MINIFIED;
  162. // Apple crash report endpoint
  163. const appleCrashEndpoint = `/projects/${organization.slug}/${projectId}/events/${eventId}/apple-crash-report?minified=${minified}`;
  164. const rawStackTraceDownloadLink = `${api.baseUrl}${appleCrashEndpoint}&download=1`;
  165. const sortByTooltip = !hasNewestFirst
  166. ? t('Not available on stack trace with single frame')
  167. : state.display.includes('raw-stack-trace')
  168. ? t('Not available on raw stack trace')
  169. : undefined;
  170. const childProps = {
  171. recentFirst: state.sortBy === 'recent-first',
  172. display: state.display,
  173. fullStackTrace: state.fullStackTrace,
  174. };
  175. return (
  176. <EventDataSection
  177. type={type}
  178. title={
  179. <Header>
  180. <Title>{cloneElement(title, {type})}</Title>
  181. <ActionWrapper>
  182. {!stackTraceNotFound && (
  183. <Fragment>
  184. {!state.display.includes('raw-stack-trace') && (
  185. <Tooltip
  186. title={t('Only full version available')}
  187. disabled={hasAppOnlyFrames}
  188. >
  189. <ButtonBar active={state.fullStackTrace ? 'full' : 'relevant'} merged>
  190. <Button
  191. type="button"
  192. size="xs"
  193. barId="relevant"
  194. onClick={() =>
  195. setState({
  196. ...state,
  197. fullStackTrace: false,
  198. })
  199. }
  200. disabled={!hasAppOnlyFrames}
  201. >
  202. {t('Most Relevant')}
  203. </Button>
  204. <Button
  205. type="button"
  206. size="xs"
  207. barId="full"
  208. priority={!hasAppOnlyFrames ? 'primary' : undefined}
  209. onClick={() =>
  210. setState({
  211. ...state,
  212. fullStackTrace: true,
  213. })
  214. }
  215. >
  216. {t('Full Stack Trace')}
  217. </Button>
  218. </ButtonBar>
  219. </Tooltip>
  220. )}
  221. {state.display.includes('raw-stack-trace') && nativePlatform && (
  222. <Button
  223. size="xs"
  224. href={rawStackTraceDownloadLink}
  225. title={t('Download raw stack trace file')}
  226. >
  227. {t('Download')}
  228. </Button>
  229. )}
  230. <CompactSelect
  231. triggerProps={{
  232. icon: <IconSort />,
  233. size: 'xs',
  234. title: sortByTooltip,
  235. }}
  236. isDisabled={!!sortByTooltip}
  237. position="bottom-end"
  238. onChange={selectedOption => {
  239. setState({...state, sortBy: selectedOption.value});
  240. }}
  241. value={state.sortBy}
  242. options={Object.entries(sortByOptions).map(([value, label]) => ({
  243. label,
  244. value: value as keyof typeof sortByOptions,
  245. }))}
  246. />
  247. <CompositeSelect
  248. triggerProps={{
  249. icon: <IconEllipsis />,
  250. size: 'xs',
  251. showChevron: false,
  252. 'aria-label': t('Options'),
  253. }}
  254. triggerLabel=""
  255. position="bottom-end"
  256. sections={[
  257. {
  258. label: t('Display'),
  259. value: 'display',
  260. defaultValue: state.display,
  261. multiple: true,
  262. options: getDisplayOptions().map(option => ({
  263. ...option,
  264. value: String(option.value),
  265. })),
  266. onChange: display => setState({...state, display}),
  267. },
  268. ]}
  269. />
  270. </Fragment>
  271. )}
  272. </ActionWrapper>
  273. </Header>
  274. }
  275. showPermalink={false}
  276. wrapTitle={wrapTitle}
  277. >
  278. <TraceEventDataSectionContext.Provider value={childProps}>
  279. {children(childProps)}
  280. </TraceEventDataSectionContext.Provider>
  281. </EventDataSection>
  282. );
  283. }
  284. interface PermalinkTitleProps
  285. extends React.DetailedHTMLProps<
  286. AnchorHTMLAttributes<HTMLAnchorElement>,
  287. HTMLAnchorElement
  288. > {}
  289. export function PermalinkTitle(props: PermalinkTitleProps) {
  290. return (
  291. <Permalink {...props} href={'#' + props.type} className="permalink">
  292. <StyledIconLink size="xs" color="subText" />
  293. <h3>{props.children}</h3>
  294. </Permalink>
  295. );
  296. }
  297. const StyledIconLink = styled(IconLink)`
  298. display: none;
  299. position: absolute;
  300. top: 50%;
  301. left: -${space(2)};
  302. transform: translateY(-50%);
  303. `;
  304. const Permalink = styled('a')`
  305. display: inline-flex;
  306. justify-content: flex-start;
  307. &:hover ${StyledIconLink} {
  308. display: block;
  309. }
  310. `;
  311. const Header = styled('div')`
  312. width: 100%;
  313. display: flex;
  314. flex-wrap: wrap;
  315. gap: ${space(1)};
  316. align-items: center;
  317. justify-content: space-between;
  318. `;
  319. const Title = styled('div')`
  320. flex: 1;
  321. @media (min-width: ${props => props.theme.breakpoints.small}) {
  322. flex: unset;
  323. }
  324. `;
  325. const ActionWrapper = styled('div')`
  326. display: flex;
  327. gap: ${space(1)};
  328. `;