traceEventDataSection.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. import {createContext, useCallback, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {LinkButton} from 'sentry/components/button';
  4. import ButtonBar from 'sentry/components/buttonBar';
  5. import {CompactSelect} from 'sentry/components/compactSelect';
  6. import {SegmentedControl} from 'sentry/components/segmentedControl';
  7. import {Tooltip} from 'sentry/components/tooltip';
  8. import {IconEllipsis, IconSort} from 'sentry/icons';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import type {Event} from 'sentry/types/event';
  12. import type {PlatformKey, Project} from 'sentry/types/project';
  13. import {trackAnalytics} from 'sentry/utils/analytics';
  14. import {isMobilePlatform, isNativePlatform} from 'sentry/utils/platform';
  15. import useApi from 'sentry/utils/useApi';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
  18. import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
  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.ReactNode;
  51. type: string;
  52. isNestedSection?: boolean;
  53. };
  54. export const TraceEventDataSectionContext = createContext<ChildProps | undefined>(
  55. undefined
  56. );
  57. export function TraceEventDataSection({
  58. type,
  59. title,
  60. stackTraceNotFound,
  61. fullStackTrace,
  62. recentFirst,
  63. children,
  64. platform,
  65. projectSlug,
  66. eventId,
  67. hasNewestFirst,
  68. hasMinified,
  69. hasVerboseFunctionNames,
  70. hasAbsoluteFilePaths,
  71. hasAbsoluteAddresses,
  72. hasAppOnlyFrames,
  73. isNestedSection = false,
  74. }: Props) {
  75. const api = useApi();
  76. const organization = useOrganization();
  77. const hasStreamlinedUI = useHasStreamlinedUI();
  78. const [state, setState] = useState<State>({
  79. sortBy: recentFirst ? 'recent-first' : 'recent-last',
  80. fullStackTrace: !hasAppOnlyFrames ? true : fullStackTrace,
  81. display: [],
  82. });
  83. const isMobile = isMobilePlatform(platform);
  84. const handleFilterFramesChange = useCallback(
  85. (val: 'full' | 'relevant') => {
  86. const isFullOptionClicked = val === 'full';
  87. trackAnalytics(
  88. isFullOptionClicked
  89. ? 'stack-trace.full_stack_trace_clicked'
  90. : 'stack-trace.most_relevant_clicked',
  91. {
  92. organization,
  93. project_slug: projectSlug,
  94. platform,
  95. is_mobile: isMobile,
  96. }
  97. );
  98. setState(currentState => ({...currentState, fullStackTrace: isFullOptionClicked}));
  99. },
  100. [organization, platform, projectSlug, isMobile]
  101. );
  102. const handleSortByChange = useCallback(
  103. (val: keyof typeof sortByOptions) => {
  104. const isRecentFirst = val === 'recent-first';
  105. trackAnalytics(
  106. isRecentFirst
  107. ? 'stack-trace.sort_option_recent_first_clicked'
  108. : 'stack-trace.sort_option_recent_last_clicked',
  109. {
  110. organization,
  111. project_slug: projectSlug,
  112. platform,
  113. is_mobile: isMobile,
  114. }
  115. );
  116. setState(currentState => ({...currentState, sortBy: val}));
  117. },
  118. [organization, platform, projectSlug, isMobile]
  119. );
  120. const handleDisplayChange = useCallback(
  121. (vals: (keyof typeof displayOptions)[]) => {
  122. if (vals.includes('raw-stack-trace')) {
  123. trackAnalytics('stack-trace.display_option_raw_stack_trace_clicked', {
  124. organization,
  125. project_slug: projectSlug,
  126. platform,
  127. is_mobile: isMobile,
  128. checked: true,
  129. });
  130. } else if (state.display.includes('raw-stack-trace')) {
  131. trackAnalytics('stack-trace.display_option_raw_stack_trace_clicked', {
  132. organization,
  133. project_slug: projectSlug,
  134. platform,
  135. is_mobile: isMobile,
  136. checked: false,
  137. });
  138. }
  139. if (vals.includes('absolute-addresses')) {
  140. trackAnalytics('stack-trace.display_option_absolute_addresses_clicked', {
  141. organization,
  142. project_slug: projectSlug,
  143. platform,
  144. is_mobile: isMobile,
  145. checked: true,
  146. });
  147. } else if (state.display.includes('absolute-addresses')) {
  148. trackAnalytics('stack-trace.display_option_absolute_addresses_clicked', {
  149. organization,
  150. project_slug: projectSlug,
  151. platform,
  152. is_mobile: isMobile,
  153. checked: false,
  154. });
  155. }
  156. if (vals.includes('absolute-file-paths')) {
  157. trackAnalytics('stack-trace.display_option_absolute_file_paths_clicked', {
  158. organization,
  159. project_slug: projectSlug,
  160. platform,
  161. is_mobile: isMobile,
  162. checked: true,
  163. });
  164. } else if (state.display.includes('absolute-file-paths')) {
  165. trackAnalytics('stack-trace.display_option_absolute_file_paths_clicked', {
  166. organization,
  167. project_slug: projectSlug,
  168. platform,
  169. is_mobile: isMobile,
  170. checked: false,
  171. });
  172. }
  173. if (vals.includes('minified')) {
  174. trackAnalytics(
  175. platform.startsWith('javascript')
  176. ? 'stack-trace.display_option_minified_clicked'
  177. : 'stack-trace.display_option_unsymbolicated_clicked',
  178. {
  179. organization,
  180. project_slug: projectSlug,
  181. platform,
  182. is_mobile: isMobile,
  183. checked: true,
  184. }
  185. );
  186. } else if (state.display.includes('minified')) {
  187. trackAnalytics(
  188. platform.startsWith('javascript')
  189. ? 'stack-trace.display_option_minified_clicked'
  190. : 'stack-trace.display_option_unsymbolicated_clicked',
  191. {
  192. organization,
  193. project_slug: projectSlug,
  194. platform,
  195. is_mobile: isMobile,
  196. checked: false,
  197. }
  198. );
  199. }
  200. if (vals.includes('verbose-function-names')) {
  201. trackAnalytics('stack-trace.display_option_verbose_function_names_clicked', {
  202. organization,
  203. project_slug: projectSlug,
  204. platform,
  205. is_mobile: isMobile,
  206. checked: true,
  207. });
  208. } else if (state.display.includes('verbose-function-names')) {
  209. trackAnalytics('stack-trace.display_option_verbose_function_names_clicked', {
  210. organization,
  211. project_slug: projectSlug,
  212. platform,
  213. is_mobile: isMobile,
  214. checked: false,
  215. });
  216. }
  217. setState(currentState => ({...currentState, display: vals}));
  218. },
  219. [organization, platform, projectSlug, isMobile, state]
  220. );
  221. function getDisplayOptions(): {
  222. label: string;
  223. value: keyof typeof displayOptions;
  224. disabled?: boolean;
  225. tooltip?: string;
  226. }[] {
  227. if (
  228. platform === 'objc' ||
  229. platform === 'native' ||
  230. platform === 'cocoa' ||
  231. platform === 'nintendo-switch'
  232. ) {
  233. return [
  234. {
  235. label: displayOptions['absolute-addresses'],
  236. value: 'absolute-addresses',
  237. disabled: state.display.includes('raw-stack-trace') || !hasAbsoluteAddresses,
  238. tooltip: state.display.includes('raw-stack-trace')
  239. ? t('Not available on raw stack trace')
  240. : !hasAbsoluteAddresses
  241. ? t('Absolute addresses not available')
  242. : undefined,
  243. },
  244. {
  245. label: displayOptions['absolute-file-paths'],
  246. value: 'absolute-file-paths',
  247. disabled: state.display.includes('raw-stack-trace') || !hasAbsoluteFilePaths,
  248. tooltip: state.display.includes('raw-stack-trace')
  249. ? t('Not available on raw stack trace')
  250. : !hasAbsoluteFilePaths
  251. ? t('Absolute file paths not available')
  252. : undefined,
  253. },
  254. {
  255. label: displayOptions.minified,
  256. value: 'minified',
  257. disabled: !hasMinified,
  258. tooltip: !hasMinified ? t('Unsymbolicated version not available') : undefined,
  259. },
  260. {
  261. label: displayOptions['raw-stack-trace'],
  262. value: 'raw-stack-trace',
  263. },
  264. {
  265. label: displayOptions['verbose-function-names'],
  266. value: 'verbose-function-names',
  267. disabled: state.display.includes('raw-stack-trace') || !hasVerboseFunctionNames,
  268. tooltip: state.display.includes('raw-stack-trace')
  269. ? t('Not available on raw stack trace')
  270. : !hasVerboseFunctionNames
  271. ? t('Verbose function names not available')
  272. : undefined,
  273. },
  274. ];
  275. }
  276. if (platform.startsWith('python')) {
  277. return [
  278. {
  279. label: displayOptions['raw-stack-trace'],
  280. value: 'raw-stack-trace',
  281. },
  282. ];
  283. }
  284. // This logic might be incomplete, but according to the SDK folks, this is 99.9% of the cases
  285. if (platform.startsWith('javascript') || platform.startsWith('node')) {
  286. return [
  287. {
  288. label: t('Minified'),
  289. value: 'minified',
  290. disabled: !hasMinified,
  291. tooltip: !hasMinified ? t('Minified version not available') : undefined,
  292. },
  293. {
  294. label: displayOptions['raw-stack-trace'],
  295. value: 'raw-stack-trace',
  296. },
  297. ];
  298. }
  299. return [
  300. {
  301. label: displayOptions.minified,
  302. value: 'minified',
  303. disabled: !hasMinified,
  304. tooltip: !hasMinified ? t('Minified version not available') : undefined,
  305. },
  306. {
  307. label: displayOptions['raw-stack-trace'],
  308. value: 'raw-stack-trace',
  309. },
  310. ];
  311. }
  312. const nativePlatform = isNativePlatform(platform);
  313. const minified = state.display.includes('minified');
  314. // Apple crash report endpoint
  315. const appleCrashEndpoint = `/projects/${organization.slug}/${projectSlug}/events/${eventId}/apple-crash-report?minified=${minified}`;
  316. const rawStackTraceDownloadLink = `${api.baseUrl}${appleCrashEndpoint}&download=1`;
  317. const sortByTooltip = !hasNewestFirst
  318. ? t('Not available on stack trace with single frame')
  319. : state.display.includes('raw-stack-trace')
  320. ? t('Not available on raw stack trace')
  321. : undefined;
  322. const childProps = {
  323. recentFirst: state.sortBy === 'recent-first',
  324. display: state.display,
  325. fullStackTrace: state.fullStackTrace,
  326. };
  327. const SectionComponent = isNestedSection ? InlineThreadSection : InterimSection;
  328. return (
  329. <SectionComponent
  330. type={type}
  331. showPermalink={!hasStreamlinedUI}
  332. title={title}
  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. <LinkButton
  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. </LinkButton>
  372. )}
  373. <CompactSelect
  374. triggerProps={{
  375. icon: <IconSort />,
  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 />,
  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. >
  408. <TraceEventDataSectionContext.Provider value={childProps}>
  409. {children(childProps)}
  410. </TraceEventDataSectionContext.Provider>
  411. </SectionComponent>
  412. );
  413. }
  414. function InlineThreadSection({
  415. children,
  416. title,
  417. actions,
  418. }: {
  419. actions: React.ReactNode;
  420. children: React.ReactNode;
  421. title: React.ReactNode;
  422. }) {
  423. return (
  424. <Wrapper>
  425. <InlineSectionHeaderWrapper>
  426. <ThreadHeading>{title}</ThreadHeading>
  427. {actions}
  428. </InlineSectionHeaderWrapper>
  429. {children}
  430. </Wrapper>
  431. );
  432. }
  433. const Wrapper = styled('div')``;
  434. const ThreadHeading = styled('h3')`
  435. color: ${p => p.theme.subText};
  436. font-size: ${p => p.theme.fontSizeMedium};
  437. font-weight: ${p => p.theme.fontWeightBold};
  438. margin-bottom: ${space(1)};
  439. `;
  440. const InlineSectionHeaderWrapper = styled('div')`
  441. display: flex;
  442. justify-content: space-between;
  443. align-items: center;
  444. margin-bottom: ${space(1)};
  445. `;