traceEventDataSection.tsx 11 KB

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