traceEventDataSection.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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. size="xs"
  192. barId="relevant"
  193. onClick={() =>
  194. setState({
  195. ...state,
  196. fullStackTrace: false,
  197. })
  198. }
  199. disabled={!hasAppOnlyFrames}
  200. >
  201. {t('Most Relevant')}
  202. </Button>
  203. <Button
  204. size="xs"
  205. barId="full"
  206. priority={!hasAppOnlyFrames ? 'primary' : undefined}
  207. onClick={() =>
  208. setState({
  209. ...state,
  210. fullStackTrace: true,
  211. })
  212. }
  213. >
  214. {t('Full Stack Trace')}
  215. </Button>
  216. </ButtonBar>
  217. </Tooltip>
  218. )}
  219. {state.display.includes('raw-stack-trace') && nativePlatform && (
  220. <Button
  221. size="xs"
  222. href={rawStackTraceDownloadLink}
  223. title={t('Download raw stack trace file')}
  224. >
  225. {t('Download')}
  226. </Button>
  227. )}
  228. <CompactSelect
  229. triggerProps={{
  230. icon: <IconSort size="xs" />,
  231. size: 'xs',
  232. title: sortByTooltip,
  233. }}
  234. isDisabled={!!sortByTooltip}
  235. position="bottom-end"
  236. onChange={selectedOption => {
  237. setState({...state, sortBy: selectedOption.value});
  238. }}
  239. value={state.sortBy}
  240. options={Object.entries(sortByOptions).map(([value, label]) => ({
  241. label,
  242. value: value as keyof typeof sortByOptions,
  243. }))}
  244. />
  245. <CompositeSelect
  246. triggerProps={{
  247. icon: <IconEllipsis size="xs" />,
  248. size: 'xs',
  249. showChevron: false,
  250. 'aria-label': t('Options'),
  251. }}
  252. triggerLabel=""
  253. position="bottom-end"
  254. sections={[
  255. {
  256. label: t('Display'),
  257. value: 'display',
  258. defaultValue: state.display,
  259. multiple: true,
  260. options: getDisplayOptions().map(option => ({
  261. ...option,
  262. value: String(option.value),
  263. })),
  264. onChange: display => setState({...state, display}),
  265. },
  266. ]}
  267. />
  268. </Fragment>
  269. )}
  270. </ActionWrapper>
  271. </Header>
  272. }
  273. showPermalink={false}
  274. wrapTitle={wrapTitle}
  275. >
  276. <TraceEventDataSectionContext.Provider value={childProps}>
  277. {children(childProps)}
  278. </TraceEventDataSectionContext.Provider>
  279. </EventDataSection>
  280. );
  281. }
  282. interface PermalinkTitleProps
  283. extends React.DetailedHTMLProps<
  284. AnchorHTMLAttributes<HTMLAnchorElement>,
  285. HTMLAnchorElement
  286. > {}
  287. export function PermalinkTitle(props: PermalinkTitleProps) {
  288. return (
  289. <Permalink {...props} href={'#' + props.type} className="permalink">
  290. <StyledIconLink size="xs" color="subText" />
  291. <h3>{props.children}</h3>
  292. </Permalink>
  293. );
  294. }
  295. const StyledIconLink = styled(IconLink)`
  296. display: none;
  297. position: absolute;
  298. top: 50%;
  299. left: -${space(2)};
  300. transform: translateY(-50%);
  301. `;
  302. const Permalink = styled('a')`
  303. display: inline-flex;
  304. justify-content: flex-start;
  305. &:hover ${StyledIconLink} {
  306. display: block;
  307. }
  308. `;
  309. const Header = styled('div')`
  310. width: 100%;
  311. display: flex;
  312. flex-wrap: wrap;
  313. gap: ${space(1)};
  314. align-items: center;
  315. justify-content: space-between;
  316. `;
  317. const Title = styled('div')`
  318. flex: 1;
  319. @media (min-width: ${props => props.theme.breakpoints.small}) {
  320. flex: unset;
  321. }
  322. `;
  323. const ActionWrapper = styled('div')`
  324. display: flex;
  325. gap: ${space(1)};
  326. `;