traceEventDataSection.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. import type {AnchorHTMLAttributes} from 'react';
  2. import {cloneElement, createContext, useCallback, useState} from 'react';
  3. import styled from '@emotion/styled';
  4. import {Button} from 'sentry/components/button';
  5. import ButtonBar from 'sentry/components/buttonBar';
  6. import {CompactSelect} from 'sentry/components/compactSelect';
  7. import {SegmentedControl} from 'sentry/components/segmentedControl';
  8. import {Tooltip} from 'sentry/components/tooltip';
  9. import {IconEllipsis, IconLink, IconSort} from 'sentry/icons';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import type {Event} from 'sentry/types/event';
  13. import type {PlatformKey, Project} from 'sentry/types/project';
  14. import {trackAnalytics} from 'sentry/utils/analytics';
  15. import {isMobilePlatform, isNativePlatform} from 'sentry/utils/platform';
  16. import useApi from 'sentry/utils/useApi';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import {EventDataSection} from './eventDataSection';
  19. const sortByOptions = {
  20. 'recent-first': t('Newest'),
  21. 'recent-last': t('Oldest'),
  22. };
  23. export const displayOptions = {
  24. 'absolute-addresses': t('Absolute addresses'),
  25. 'absolute-file-paths': t('Absolute file paths'),
  26. minified: t('Unsymbolicated'),
  27. 'raw-stack-trace': t('Raw stack trace'),
  28. 'verbose-function-names': t('Verbose function names'),
  29. };
  30. type State = {
  31. display: Array<keyof typeof displayOptions>;
  32. fullStackTrace: boolean;
  33. sortBy: keyof typeof sortByOptions;
  34. };
  35. type ChildProps = Omit<State, 'sortBy'> & {recentFirst: boolean};
  36. type Props = {
  37. children: (childProps: ChildProps) => React.ReactNode;
  38. eventId: Event['id'];
  39. fullStackTrace: boolean;
  40. hasAbsoluteAddresses: boolean;
  41. hasAbsoluteFilePaths: boolean;
  42. hasAppOnlyFrames: boolean;
  43. hasMinified: boolean;
  44. hasNewestFirst: boolean;
  45. hasVerboseFunctionNames: boolean;
  46. platform: PlatformKey;
  47. projectSlug: Project['slug'];
  48. recentFirst: boolean;
  49. stackTraceNotFound: boolean;
  50. title: React.ReactElement<any, any>;
  51. type: string;
  52. wrapTitle?: boolean;
  53. };
  54. export const TraceEventDataSectionContext = createContext<ChildProps | undefined>(
  55. undefined
  56. );
  57. export function TraceEventDataSection({
  58. type,
  59. title,
  60. wrapTitle,
  61. stackTraceNotFound,
  62. fullStackTrace,
  63. recentFirst,
  64. children,
  65. platform,
  66. projectSlug,
  67. eventId,
  68. hasNewestFirst,
  69. hasMinified,
  70. hasVerboseFunctionNames,
  71. hasAbsoluteFilePaths,
  72. hasAbsoluteAddresses,
  73. hasAppOnlyFrames,
  74. }: Props) {
  75. const api = useApi();
  76. const organization = useOrganization();
  77. const [state, setState] = useState<State>({
  78. sortBy: recentFirst ? 'recent-first' : 'recent-last',
  79. fullStackTrace: !hasAppOnlyFrames ? true : fullStackTrace,
  80. display: [],
  81. });
  82. const isMobile = isMobilePlatform(platform);
  83. const handleFilterFramesChange = useCallback(
  84. (val: 'full' | 'relevant') => {
  85. const isFullOptionClicked = val === 'full';
  86. trackAnalytics(
  87. isFullOptionClicked
  88. ? 'stack-trace.full_stack_trace_clicked'
  89. : 'stack-trace.most_relevant_clicked',
  90. {
  91. organization,
  92. project_slug: projectSlug,
  93. platform,
  94. is_mobile: isMobile,
  95. }
  96. );
  97. setState(currentState => ({...currentState, fullStackTrace: isFullOptionClicked}));
  98. },
  99. [organization, platform, projectSlug, isMobile]
  100. );
  101. const handleSortByChange = useCallback(
  102. (val: keyof typeof sortByOptions) => {
  103. const isRecentFirst = val === 'recent-first';
  104. trackAnalytics(
  105. isRecentFirst
  106. ? 'stack-trace.sort_option_recent_first_clicked'
  107. : 'stack-trace.sort_option_recent_last_clicked',
  108. {
  109. organization,
  110. project_slug: projectSlug,
  111. platform,
  112. is_mobile: isMobile,
  113. }
  114. );
  115. setState(currentState => ({...currentState, sortBy: val}));
  116. },
  117. [organization, platform, projectSlug, isMobile]
  118. );
  119. const handleDisplayChange = useCallback(
  120. (vals: (keyof typeof displayOptions)[]) => {
  121. if (vals.includes('raw-stack-trace')) {
  122. trackAnalytics('stack-trace.display_option_raw_stack_trace_clicked', {
  123. organization,
  124. project_slug: projectSlug,
  125. platform,
  126. is_mobile: isMobile,
  127. checked: true,
  128. });
  129. } else if (state.display.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: false,
  136. });
  137. }
  138. if (vals.includes('absolute-addresses')) {
  139. trackAnalytics('stack-trace.display_option_absolute_addresses_clicked', {
  140. organization,
  141. project_slug: projectSlug,
  142. platform,
  143. is_mobile: isMobile,
  144. checked: true,
  145. });
  146. } else if (state.display.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: false,
  153. });
  154. }
  155. if (vals.includes('absolute-file-paths')) {
  156. trackAnalytics('stack-trace.display_option_absolute_file_paths_clicked', {
  157. organization,
  158. project_slug: projectSlug,
  159. platform,
  160. is_mobile: isMobile,
  161. checked: true,
  162. });
  163. } else if (state.display.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: false,
  170. });
  171. }
  172. if (vals.includes('minified')) {
  173. trackAnalytics(
  174. platform.startsWith('javascript')
  175. ? 'stack-trace.display_option_minified_clicked'
  176. : 'stack-trace.display_option_unsymbolicated_clicked',
  177. {
  178. organization,
  179. project_slug: projectSlug,
  180. platform,
  181. is_mobile: isMobile,
  182. checked: true,
  183. }
  184. );
  185. } else if (state.display.includes('minified')) {
  186. trackAnalytics(
  187. platform.startsWith('javascript')
  188. ? 'stack-trace.display_option_minified_clicked'
  189. : 'stack-trace.display_option_unsymbolicated_clicked',
  190. {
  191. organization,
  192. project_slug: projectSlug,
  193. platform,
  194. is_mobile: isMobile,
  195. checked: false,
  196. }
  197. );
  198. }
  199. if (vals.includes('verbose-function-names')) {
  200. trackAnalytics('stack-trace.display_option_verbose_function_names_clicked', {
  201. organization,
  202. project_slug: projectSlug,
  203. platform,
  204. is_mobile: isMobile,
  205. checked: true,
  206. });
  207. } else if (state.display.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: false,
  214. });
  215. }
  216. setState(currentState => ({...currentState, display: vals}));
  217. },
  218. [organization, platform, projectSlug, isMobile, state]
  219. );
  220. function getDisplayOptions(): {
  221. label: string;
  222. value: keyof typeof displayOptions;
  223. disabled?: boolean;
  224. tooltip?: string;
  225. }[] {
  226. if (
  227. platform === 'objc' ||
  228. platform === 'native' ||
  229. platform === 'cocoa' ||
  230. platform === 'nintendo-switch'
  231. ) {
  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. guideTarget="stacktrace"
  331. actions={
  332. !stackTraceNotFound && (
  333. <ButtonBar gap={1}>
  334. {!state.display.includes('raw-stack-trace') && (
  335. <Tooltip
  336. title={t('Only full version available')}
  337. disabled={hasAppOnlyFrames}
  338. >
  339. <SegmentedControl
  340. size="xs"
  341. aria-label={t('Filter frames')}
  342. value={state.fullStackTrace ? 'full' : 'relevant'}
  343. onChange={handleFilterFramesChange}
  344. >
  345. <SegmentedControl.Item key="relevant" disabled={!hasAppOnlyFrames}>
  346. {t('Most Relevant')}
  347. </SegmentedControl.Item>
  348. <SegmentedControl.Item key="full">
  349. {t('Full Stack Trace')}
  350. </SegmentedControl.Item>
  351. </SegmentedControl>
  352. </Tooltip>
  353. )}
  354. {state.display.includes('raw-stack-trace') && nativePlatform && (
  355. <Button
  356. size="xs"
  357. href={rawStackTraceDownloadLink}
  358. title={t('Download raw stack trace file')}
  359. onClick={() => {
  360. trackAnalytics('stack-trace.download_clicked', {
  361. organization,
  362. project_slug: projectSlug,
  363. platform,
  364. is_mobile: isMobile,
  365. });
  366. }}
  367. >
  368. {t('Download')}
  369. </Button>
  370. )}
  371. <CompactSelect
  372. triggerProps={{
  373. icon: <IconSort />,
  374. size: 'xs',
  375. title: sortByTooltip,
  376. }}
  377. disabled={!!sortByTooltip}
  378. position="bottom-end"
  379. onChange={selectedOption => {
  380. handleSortByChange(selectedOption.value);
  381. }}
  382. value={state.sortBy}
  383. options={Object.entries(sortByOptions).map(([value, label]) => ({
  384. label,
  385. value: value as keyof typeof sortByOptions,
  386. }))}
  387. />
  388. <CompactSelect
  389. triggerProps={{
  390. icon: <IconEllipsis />,
  391. size: 'xs',
  392. showChevron: false,
  393. 'aria-label': t('Options'),
  394. }}
  395. multiple
  396. triggerLabel=""
  397. position="bottom-end"
  398. value={state.display}
  399. onChange={opts => handleDisplayChange(opts.map(opt => opt.value))}
  400. options={[{label: t('Display'), options: getDisplayOptions()}]}
  401. />
  402. </ButtonBar>
  403. )
  404. }
  405. showPermalink={false}
  406. wrapTitle={wrapTitle}
  407. >
  408. <TraceEventDataSectionContext.Provider value={childProps}>
  409. {children(childProps)}
  410. </TraceEventDataSectionContext.Provider>
  411. </EventDataSection>
  412. );
  413. }
  414. interface PermalinkTitleProps
  415. extends React.DetailedHTMLProps<
  416. AnchorHTMLAttributes<HTMLAnchorElement>,
  417. HTMLAnchorElement
  418. > {}
  419. export function PermalinkTitle(props: PermalinkTitleProps) {
  420. return (
  421. <Permalink {...props} href={'#' + props.type} className="permalink">
  422. <StyledIconLink size="xs" color="subText" />
  423. <h3>{props.children}</h3>
  424. </Permalink>
  425. );
  426. }
  427. const StyledIconLink = styled(IconLink)`
  428. display: none;
  429. position: absolute;
  430. top: 50%;
  431. left: -${space(2)};
  432. transform: translateY(-50%);
  433. `;
  434. const Permalink = styled('a')`
  435. display: inline-flex;
  436. justify-content: flex-start;
  437. &:hover ${StyledIconLink} {
  438. display: block;
  439. }
  440. `;