traceEventDataSection.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. import {createContext, useCallback, useState} from 'react';
  2. import {Button} from 'sentry/components/button';
  3. import ButtonBar from 'sentry/components/buttonBar';
  4. import {CompactSelect} from 'sentry/components/compactSelect';
  5. import {SegmentedControl} from 'sentry/components/segmentedControl';
  6. import {Tooltip} from 'sentry/components/tooltip';
  7. import {IconEllipsis, IconSort} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import type {Event} from 'sentry/types/event';
  10. import type {PlatformKey, Project} from 'sentry/types/project';
  11. import {trackAnalytics} from 'sentry/utils/analytics';
  12. import {isMobilePlatform, isNativePlatform} from 'sentry/utils/platform';
  13. import useApi from 'sentry/utils/useApi';
  14. import useOrganization from 'sentry/utils/useOrganization';
  15. import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
  16. const sortByOptions = {
  17. 'recent-first': t('Newest'),
  18. 'recent-last': t('Oldest'),
  19. };
  20. export const displayOptions = {
  21. 'absolute-addresses': t('Absolute addresses'),
  22. 'absolute-file-paths': t('Absolute file paths'),
  23. minified: t('Unsymbolicated'),
  24. 'raw-stack-trace': t('Raw stack trace'),
  25. 'verbose-function-names': t('Verbose function names'),
  26. };
  27. type State = {
  28. display: Array<keyof typeof displayOptions>;
  29. fullStackTrace: boolean;
  30. sortBy: keyof typeof sortByOptions;
  31. };
  32. type ChildProps = Omit<State, 'sortBy'> & {recentFirst: boolean};
  33. type Props = {
  34. children: (childProps: ChildProps) => React.ReactNode;
  35. eventId: Event['id'];
  36. fullStackTrace: boolean;
  37. hasAbsoluteAddresses: boolean;
  38. hasAbsoluteFilePaths: boolean;
  39. hasAppOnlyFrames: boolean;
  40. hasMinified: boolean;
  41. hasNewestFirst: boolean;
  42. hasVerboseFunctionNames: boolean;
  43. platform: PlatformKey;
  44. projectSlug: Project['slug'];
  45. recentFirst: boolean;
  46. stackTraceNotFound: boolean;
  47. title: React.ReactNode;
  48. type: string;
  49. wrapTitle?: boolean;
  50. };
  51. export const TraceEventDataSectionContext = createContext<ChildProps | undefined>(
  52. undefined
  53. );
  54. export function TraceEventDataSection({
  55. type,
  56. title,
  57. wrapTitle,
  58. stackTraceNotFound,
  59. fullStackTrace,
  60. recentFirst,
  61. children,
  62. platform,
  63. projectSlug,
  64. eventId,
  65. hasNewestFirst,
  66. hasMinified,
  67. hasVerboseFunctionNames,
  68. hasAbsoluteFilePaths,
  69. hasAbsoluteAddresses,
  70. hasAppOnlyFrames,
  71. }: Props) {
  72. const api = useApi();
  73. const organization = useOrganization();
  74. const [state, setState] = useState<State>({
  75. sortBy: recentFirst ? 'recent-first' : 'recent-last',
  76. fullStackTrace: !hasAppOnlyFrames ? true : fullStackTrace,
  77. display: [],
  78. });
  79. const isMobile = isMobilePlatform(platform);
  80. const handleFilterFramesChange = useCallback(
  81. (val: 'full' | 'relevant') => {
  82. const isFullOptionClicked = val === 'full';
  83. trackAnalytics(
  84. isFullOptionClicked
  85. ? 'stack-trace.full_stack_trace_clicked'
  86. : 'stack-trace.most_relevant_clicked',
  87. {
  88. organization,
  89. project_slug: projectSlug,
  90. platform,
  91. is_mobile: isMobile,
  92. }
  93. );
  94. setState(currentState => ({...currentState, fullStackTrace: isFullOptionClicked}));
  95. },
  96. [organization, platform, projectSlug, isMobile]
  97. );
  98. const handleSortByChange = useCallback(
  99. (val: keyof typeof sortByOptions) => {
  100. const isRecentFirst = val === 'recent-first';
  101. trackAnalytics(
  102. isRecentFirst
  103. ? 'stack-trace.sort_option_recent_first_clicked'
  104. : 'stack-trace.sort_option_recent_last_clicked',
  105. {
  106. organization,
  107. project_slug: projectSlug,
  108. platform,
  109. is_mobile: isMobile,
  110. }
  111. );
  112. setState(currentState => ({...currentState, sortBy: val}));
  113. },
  114. [organization, platform, projectSlug, isMobile]
  115. );
  116. const handleDisplayChange = useCallback(
  117. (vals: (keyof typeof displayOptions)[]) => {
  118. if (vals.includes('raw-stack-trace')) {
  119. trackAnalytics('stack-trace.display_option_raw_stack_trace_clicked', {
  120. organization,
  121. project_slug: projectSlug,
  122. platform,
  123. is_mobile: isMobile,
  124. checked: true,
  125. });
  126. } else if (state.display.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: false,
  133. });
  134. }
  135. if (vals.includes('absolute-addresses')) {
  136. trackAnalytics('stack-trace.display_option_absolute_addresses_clicked', {
  137. organization,
  138. project_slug: projectSlug,
  139. platform,
  140. is_mobile: isMobile,
  141. checked: true,
  142. });
  143. } else if (state.display.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: false,
  150. });
  151. }
  152. if (vals.includes('absolute-file-paths')) {
  153. trackAnalytics('stack-trace.display_option_absolute_file_paths_clicked', {
  154. organization,
  155. project_slug: projectSlug,
  156. platform,
  157. is_mobile: isMobile,
  158. checked: true,
  159. });
  160. } else if (state.display.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: false,
  167. });
  168. }
  169. if (vals.includes('minified')) {
  170. trackAnalytics(
  171. platform.startsWith('javascript')
  172. ? 'stack-trace.display_option_minified_clicked'
  173. : 'stack-trace.display_option_unsymbolicated_clicked',
  174. {
  175. organization,
  176. project_slug: projectSlug,
  177. platform,
  178. is_mobile: isMobile,
  179. checked: true,
  180. }
  181. );
  182. } else if (state.display.includes('minified')) {
  183. trackAnalytics(
  184. platform.startsWith('javascript')
  185. ? 'stack-trace.display_option_minified_clicked'
  186. : 'stack-trace.display_option_unsymbolicated_clicked',
  187. {
  188. organization,
  189. project_slug: projectSlug,
  190. platform,
  191. is_mobile: isMobile,
  192. checked: false,
  193. }
  194. );
  195. }
  196. if (vals.includes('verbose-function-names')) {
  197. trackAnalytics('stack-trace.display_option_verbose_function_names_clicked', {
  198. organization,
  199. project_slug: projectSlug,
  200. platform,
  201. is_mobile: isMobile,
  202. checked: true,
  203. });
  204. } else if (state.display.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: false,
  211. });
  212. }
  213. setState(currentState => ({...currentState, display: vals}));
  214. },
  215. [organization, platform, projectSlug, isMobile, state]
  216. );
  217. function getDisplayOptions(): {
  218. label: string;
  219. value: keyof typeof displayOptions;
  220. disabled?: boolean;
  221. tooltip?: string;
  222. }[] {
  223. if (
  224. platform === 'objc' ||
  225. platform === 'native' ||
  226. platform === 'cocoa' ||
  227. platform === 'nintendo-switch'
  228. ) {
  229. return [
  230. {
  231. label: displayOptions['absolute-addresses'],
  232. value: 'absolute-addresses',
  233. disabled: state.display.includes('raw-stack-trace') || !hasAbsoluteAddresses,
  234. tooltip: state.display.includes('raw-stack-trace')
  235. ? t('Not available on raw stack trace')
  236. : !hasAbsoluteAddresses
  237. ? t('Absolute addresses not available')
  238. : undefined,
  239. },
  240. {
  241. label: displayOptions['absolute-file-paths'],
  242. value: 'absolute-file-paths',
  243. disabled: state.display.includes('raw-stack-trace') || !hasAbsoluteFilePaths,
  244. tooltip: state.display.includes('raw-stack-trace')
  245. ? t('Not available on raw stack trace')
  246. : !hasAbsoluteFilePaths
  247. ? t('Absolute file paths not available')
  248. : undefined,
  249. },
  250. {
  251. label: displayOptions.minified,
  252. value: 'minified',
  253. disabled: !hasMinified,
  254. tooltip: !hasMinified ? t('Unsymbolicated version not available') : undefined,
  255. },
  256. {
  257. label: displayOptions['raw-stack-trace'],
  258. value: 'raw-stack-trace',
  259. },
  260. {
  261. label: displayOptions['verbose-function-names'],
  262. value: 'verbose-function-names',
  263. disabled: state.display.includes('raw-stack-trace') || !hasVerboseFunctionNames,
  264. tooltip: state.display.includes('raw-stack-trace')
  265. ? t('Not available on raw stack trace')
  266. : !hasVerboseFunctionNames
  267. ? t('Verbose function names not available')
  268. : undefined,
  269. },
  270. ];
  271. }
  272. if (platform.startsWith('python')) {
  273. return [
  274. {
  275. label: displayOptions['raw-stack-trace'],
  276. value: 'raw-stack-trace',
  277. },
  278. ];
  279. }
  280. // This logic might be incomplete, but according to the SDK folks, this is 99.9% of the cases
  281. if (platform.startsWith('javascript') || platform.startsWith('node')) {
  282. return [
  283. {
  284. label: t('Minified'),
  285. value: 'minified',
  286. disabled: !hasMinified,
  287. tooltip: !hasMinified ? t('Minified version not available') : undefined,
  288. },
  289. {
  290. label: displayOptions['raw-stack-trace'],
  291. value: 'raw-stack-trace',
  292. },
  293. ];
  294. }
  295. return [
  296. {
  297. label: displayOptions.minified,
  298. value: 'minified',
  299. disabled: !hasMinified,
  300. tooltip: !hasMinified ? t('Minified version not available') : undefined,
  301. },
  302. {
  303. label: displayOptions['raw-stack-trace'],
  304. value: 'raw-stack-trace',
  305. },
  306. ];
  307. }
  308. const nativePlatform = isNativePlatform(platform);
  309. const minified = state.display.includes('minified');
  310. // Apple crash report endpoint
  311. const appleCrashEndpoint = `/projects/${organization.slug}/${projectSlug}/events/${eventId}/apple-crash-report?minified=${minified}`;
  312. const rawStackTraceDownloadLink = `${api.baseUrl}${appleCrashEndpoint}&download=1`;
  313. const sortByTooltip = !hasNewestFirst
  314. ? t('Not available on stack trace with single frame')
  315. : state.display.includes('raw-stack-trace')
  316. ? t('Not available on raw stack trace')
  317. : undefined;
  318. const childProps = {
  319. recentFirst: state.sortBy === 'recent-first',
  320. display: state.display,
  321. fullStackTrace: state.fullStackTrace,
  322. };
  323. return (
  324. <InterimSection
  325. type={type}
  326. title={title}
  327. guideTarget={type}
  328. actions={
  329. !stackTraceNotFound && (
  330. <ButtonBar gap={1}>
  331. {!state.display.includes('raw-stack-trace') && (
  332. <Tooltip
  333. title={t('Only full version available')}
  334. disabled={hasAppOnlyFrames}
  335. >
  336. <SegmentedControl
  337. size="xs"
  338. aria-label={t('Filter frames')}
  339. value={state.fullStackTrace ? 'full' : 'relevant'}
  340. onChange={handleFilterFramesChange}
  341. >
  342. <SegmentedControl.Item key="relevant" disabled={!hasAppOnlyFrames}>
  343. {t('Most Relevant')}
  344. </SegmentedControl.Item>
  345. <SegmentedControl.Item key="full">
  346. {t('Full Stack Trace')}
  347. </SegmentedControl.Item>
  348. </SegmentedControl>
  349. </Tooltip>
  350. )}
  351. {state.display.includes('raw-stack-trace') && nativePlatform && (
  352. <Button
  353. size="xs"
  354. href={rawStackTraceDownloadLink}
  355. title={t('Download raw stack trace file')}
  356. onClick={() => {
  357. trackAnalytics('stack-trace.download_clicked', {
  358. organization,
  359. project_slug: projectSlug,
  360. platform,
  361. is_mobile: isMobile,
  362. });
  363. }}
  364. >
  365. {t('Download')}
  366. </Button>
  367. )}
  368. <CompactSelect
  369. triggerProps={{
  370. icon: <IconSort />,
  371. size: 'xs',
  372. title: sortByTooltip,
  373. }}
  374. disabled={!!sortByTooltip}
  375. position="bottom-end"
  376. onChange={selectedOption => {
  377. handleSortByChange(selectedOption.value);
  378. }}
  379. value={state.sortBy}
  380. options={Object.entries(sortByOptions).map(([value, label]) => ({
  381. label,
  382. value: value as keyof typeof sortByOptions,
  383. }))}
  384. />
  385. <CompactSelect
  386. triggerProps={{
  387. icon: <IconEllipsis />,
  388. size: 'xs',
  389. showChevron: false,
  390. 'aria-label': t('Options'),
  391. }}
  392. multiple
  393. triggerLabel=""
  394. position="bottom-end"
  395. value={state.display}
  396. onChange={opts => handleDisplayChange(opts.map(opt => opt.value))}
  397. options={[{label: t('Display'), options: getDisplayOptions()}]}
  398. />
  399. </ButtonBar>
  400. )
  401. }
  402. wrapTitle={wrapTitle}
  403. >
  404. <TraceEventDataSectionContext.Provider value={childProps}>
  405. {children(childProps)}
  406. </TraceEventDataSectionContext.Provider>
  407. </InterimSection>
  408. );
  409. }