traceEventDataSection.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. import {
  2. AnchorHTMLAttributes,
  3. cloneElement,
  4. createContext,
  5. useCallback,
  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 {SegmentedControl} from 'sentry/components/segmentedControl';
  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 {StackType} from 'sentry/types/stacktrace';
  20. import {trackAnalytics} from 'sentry/utils/analytics';
  21. import {isMobilePlatform, 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. projectSlug: Project['slug'];
  54. recentFirst: boolean;
  55. stackTraceNotFound: boolean;
  56. stackType: StackType;
  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. projectSlug,
  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. const isMobile = isMobilePlatform(platform);
  91. const handleFilterFramesChange = useCallback(
  92. (val: 'full' | 'relevant') => {
  93. const isFullOptionClicked = val === 'full';
  94. trackAnalytics(
  95. isFullOptionClicked
  96. ? 'stack-trace.full_stack_trace_clicked'
  97. : 'stack-trace.most_relevant_clicked',
  98. {
  99. organization,
  100. project_slug: projectSlug,
  101. platform,
  102. is_mobile: isMobile,
  103. }
  104. );
  105. setState(currentState => ({...currentState, fullStackTrace: isFullOptionClicked}));
  106. },
  107. [organization, platform, projectSlug, isMobile]
  108. );
  109. const handleSortByChange = useCallback(
  110. (val: keyof typeof sortByOptions) => {
  111. const isRecentFirst = val === 'recent-first';
  112. trackAnalytics(
  113. isRecentFirst
  114. ? 'stack-trace.sort_option_recent_first_clicked'
  115. : 'stack-trace.sort_option_recent_last_clicked',
  116. {
  117. organization,
  118. project_slug: projectSlug,
  119. platform,
  120. is_mobile: isMobile,
  121. }
  122. );
  123. setState(currentState => ({...currentState, sortBy: val}));
  124. },
  125. [organization, platform, projectSlug, isMobile]
  126. );
  127. const handleDisplayChange = useCallback(
  128. (vals: (keyof typeof displayOptions)[]) => {
  129. if (vals.includes('raw-stack-trace')) {
  130. trackAnalytics('stack-trace.display_option_raw_stack_trace_clicked', {
  131. organization,
  132. project_slug: projectSlug,
  133. platform,
  134. is_mobile: isMobile,
  135. checked: true,
  136. });
  137. } else if (state.display.includes('raw-stack-trace')) {
  138. trackAnalytics('stack-trace.display_option_raw_stack_trace_clicked', {
  139. organization,
  140. project_slug: projectSlug,
  141. platform,
  142. is_mobile: isMobile,
  143. checked: false,
  144. });
  145. }
  146. if (vals.includes('absolute-addresses')) {
  147. trackAnalytics('stack-trace.display_option_absolute_addresses_clicked', {
  148. organization,
  149. project_slug: projectSlug,
  150. platform,
  151. is_mobile: isMobile,
  152. checked: true,
  153. });
  154. } else if (state.display.includes('absolute-addresses')) {
  155. trackAnalytics('stack-trace.display_option_absolute_addresses_clicked', {
  156. organization,
  157. project_slug: projectSlug,
  158. platform,
  159. is_mobile: isMobile,
  160. checked: false,
  161. });
  162. }
  163. if (vals.includes('absolute-file-paths')) {
  164. trackAnalytics('stack-trace.display_option_absolute_file_paths_clicked', {
  165. organization,
  166. project_slug: projectSlug,
  167. platform,
  168. is_mobile: isMobile,
  169. checked: true,
  170. });
  171. } else if (state.display.includes('absolute-file-paths')) {
  172. trackAnalytics('stack-trace.display_option_absolute_file_paths_clicked', {
  173. organization,
  174. project_slug: projectSlug,
  175. platform,
  176. is_mobile: isMobile,
  177. checked: false,
  178. });
  179. }
  180. if (vals.includes('minified')) {
  181. trackAnalytics(
  182. platform.startsWith('javascript')
  183. ? 'stack-trace.display_option_minified_clicked'
  184. : 'stack-trace.display_option_unsymbolicated_clicked',
  185. {
  186. organization,
  187. project_slug: projectSlug,
  188. platform,
  189. is_mobile: isMobile,
  190. checked: true,
  191. }
  192. );
  193. } else if (state.display.includes('minified')) {
  194. trackAnalytics(
  195. platform.startsWith('javascript')
  196. ? 'stack-trace.display_option_minified_clicked'
  197. : 'stack-trace.display_option_unsymbolicated_clicked',
  198. {
  199. organization,
  200. project_slug: projectSlug,
  201. platform,
  202. is_mobile: isMobile,
  203. checked: false,
  204. }
  205. );
  206. }
  207. if (vals.includes('verbose-function-names')) {
  208. trackAnalytics('stack-trace.display_option_verbose_function_names_clicked', {
  209. organization,
  210. project_slug: projectSlug,
  211. platform,
  212. is_mobile: isMobile,
  213. checked: true,
  214. });
  215. } else if (state.display.includes('verbose-function-names')) {
  216. trackAnalytics('stack-trace.display_option_verbose_function_names_clicked', {
  217. organization,
  218. project_slug: projectSlug,
  219. platform,
  220. is_mobile: isMobile,
  221. checked: false,
  222. });
  223. }
  224. setState(currentState => ({...currentState, display: vals}));
  225. },
  226. [organization, platform, projectSlug, isMobile, state]
  227. );
  228. function getDisplayOptions(): {
  229. label: string;
  230. value: keyof typeof displayOptions;
  231. disabled?: boolean;
  232. tooltip?: string;
  233. }[] {
  234. if (platform === 'objc' || platform === 'native' || platform === 'cocoa') {
  235. return [
  236. {
  237. label: displayOptions['absolute-addresses'],
  238. value: 'absolute-addresses',
  239. disabled: state.display.includes('raw-stack-trace') || !hasAbsoluteAddresses,
  240. tooltip: state.display.includes('raw-stack-trace')
  241. ? t('Not available on raw stack trace')
  242. : !hasAbsoluteAddresses
  243. ? t('Absolute addresses not available')
  244. : undefined,
  245. },
  246. {
  247. label: displayOptions['absolute-file-paths'],
  248. value: 'absolute-file-paths',
  249. disabled: state.display.includes('raw-stack-trace') || !hasAbsoluteFilePaths,
  250. tooltip: state.display.includes('raw-stack-trace')
  251. ? t('Not available on raw stack trace')
  252. : !hasAbsoluteFilePaths
  253. ? t('Absolute file paths not available')
  254. : undefined,
  255. },
  256. {
  257. label: displayOptions.minified,
  258. value: 'minified',
  259. disabled: !hasMinified,
  260. tooltip: !hasMinified ? t('Unsymbolicated version not available') : undefined,
  261. },
  262. {
  263. label: displayOptions['raw-stack-trace'],
  264. value: 'raw-stack-trace',
  265. },
  266. {
  267. label: displayOptions['verbose-function-names'],
  268. value: 'verbose-function-names',
  269. disabled: state.display.includes('raw-stack-trace') || !hasVerboseFunctionNames,
  270. tooltip: state.display.includes('raw-stack-trace')
  271. ? t('Not available on raw stack trace')
  272. : !hasVerboseFunctionNames
  273. ? t('Verbose function names not available')
  274. : undefined,
  275. },
  276. ];
  277. }
  278. if (platform.startsWith('python')) {
  279. return [
  280. {
  281. label: displayOptions['raw-stack-trace'],
  282. value: 'raw-stack-trace',
  283. },
  284. ];
  285. }
  286. // This logic might be incomplete, but according to the SDK folks, this is 99.9% of the cases
  287. if (platform.startsWith('javascript') || platform.startsWith('node')) {
  288. return [
  289. {
  290. label: t('Minified'),
  291. value: 'minified',
  292. disabled: !hasMinified,
  293. tooltip: !hasMinified ? t('Minified version not available') : undefined,
  294. },
  295. {
  296. label: displayOptions['raw-stack-trace'],
  297. value: 'raw-stack-trace',
  298. },
  299. ];
  300. }
  301. return [
  302. {
  303. label: displayOptions.minified,
  304. value: 'minified',
  305. disabled: !hasMinified,
  306. tooltip: !hasMinified ? t('Minified version not available') : undefined,
  307. },
  308. {
  309. label: displayOptions['raw-stack-trace'],
  310. value: 'raw-stack-trace',
  311. },
  312. ];
  313. }
  314. const nativePlatform = isNativePlatform(platform);
  315. const minified = stackType === StackType.MINIFIED;
  316. // Apple crash report endpoint
  317. const appleCrashEndpoint = `/projects/${organization.slug}/${projectSlug}/events/${eventId}/apple-crash-report?minified=${minified}`;
  318. const rawStackTraceDownloadLink = `${api.baseUrl}${appleCrashEndpoint}&download=1`;
  319. const sortByTooltip = !hasNewestFirst
  320. ? t('Not available on stack trace with single frame')
  321. : state.display.includes('raw-stack-trace')
  322. ? t('Not available on raw stack trace')
  323. : undefined;
  324. const childProps = {
  325. recentFirst: state.sortBy === 'recent-first',
  326. display: state.display,
  327. fullStackTrace: state.fullStackTrace,
  328. };
  329. return (
  330. <EventDataSection
  331. type={type}
  332. title={cloneElement(title, {type})}
  333. actions={
  334. !stackTraceNotFound && (
  335. <ButtonBar gap={1}>
  336. {!state.display.includes('raw-stack-trace') && (
  337. <Tooltip
  338. title={t('Only full version available')}
  339. disabled={hasAppOnlyFrames}
  340. >
  341. <SegmentedControl
  342. size="xs"
  343. aria-label={t('Filter frames')}
  344. value={state.fullStackTrace ? 'full' : 'relevant'}
  345. onChange={handleFilterFramesChange}
  346. >
  347. <SegmentedControl.Item key="relevant" disabled={!hasAppOnlyFrames}>
  348. {t('Most Relevant')}
  349. </SegmentedControl.Item>
  350. <SegmentedControl.Item key="full">
  351. {t('Full Stack Trace')}
  352. </SegmentedControl.Item>
  353. </SegmentedControl>
  354. </Tooltip>
  355. )}
  356. {state.display.includes('raw-stack-trace') && nativePlatform && (
  357. <Button
  358. size="xs"
  359. href={rawStackTraceDownloadLink}
  360. title={t('Download raw stack trace file')}
  361. onClick={() => {
  362. trackAnalytics('stack-trace.download_clicked', {
  363. organization,
  364. project_slug: projectSlug,
  365. platform,
  366. is_mobile: isMobile,
  367. });
  368. }}
  369. >
  370. {t('Download')}
  371. </Button>
  372. )}
  373. <CompactSelect
  374. triggerProps={{
  375. icon: <IconSort size="xs" />,
  376. size: 'xs',
  377. title: sortByTooltip,
  378. }}
  379. disabled={!!sortByTooltip}
  380. position="bottom-end"
  381. onChange={selectedOption => {
  382. handleSortByChange(selectedOption.value);
  383. }}
  384. value={state.sortBy}
  385. options={Object.entries(sortByOptions).map(([value, label]) => ({
  386. label,
  387. value: value as keyof typeof sortByOptions,
  388. }))}
  389. />
  390. <CompactSelect
  391. triggerProps={{
  392. icon: <IconEllipsis size="xs" />,
  393. size: 'xs',
  394. showChevron: false,
  395. 'aria-label': t('Options'),
  396. }}
  397. multiple
  398. triggerLabel=""
  399. position="bottom-end"
  400. value={state.display}
  401. onChange={opts => handleDisplayChange(opts.map(opt => opt.value))}
  402. options={[{label: t('Display'), options: getDisplayOptions()}]}
  403. />
  404. </ButtonBar>
  405. )
  406. }
  407. showPermalink={false}
  408. wrapTitle={wrapTitle}
  409. >
  410. <TraceEventDataSectionContext.Provider value={childProps}>
  411. {children(childProps)}
  412. </TraceEventDataSectionContext.Provider>
  413. </EventDataSection>
  414. );
  415. }
  416. interface PermalinkTitleProps
  417. extends React.DetailedHTMLProps<
  418. AnchorHTMLAttributes<HTMLAnchorElement>,
  419. HTMLAnchorElement
  420. > {}
  421. export function PermalinkTitle(props: PermalinkTitleProps) {
  422. return (
  423. <Permalink {...props} href={'#' + props.type} className="permalink">
  424. <StyledIconLink size="xs" color="subText" />
  425. <h3>{props.children}</h3>
  426. </Permalink>
  427. );
  428. }
  429. const StyledIconLink = styled(IconLink)`
  430. display: none;
  431. position: absolute;
  432. top: 50%;
  433. left: -${space(2)};
  434. transform: translateY(-50%);
  435. `;
  436. const Permalink = styled('a')`
  437. display: inline-flex;
  438. justify-content: flex-start;
  439. &:hover ${StyledIconLink} {
  440. display: block;
  441. }
  442. `;