traceEventDataSection.tsx 9.4 KB

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