traceEventDataSection.tsx 15 KB

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