traceEventDataSection.tsx 14 KB

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