nativeFrame.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. import type {MouseEvent} from 'react';
  2. import {Fragment, useContext, useState} from 'react';
  3. import styled from '@emotion/styled';
  4. import scrollToElement from 'scroll-to-element';
  5. import {Button} from 'sentry/components/button';
  6. import ErrorBoundary from 'sentry/components/errorBoundary';
  7. import {OpenInContextLine} from 'sentry/components/events/interfaces/frame/openInContextLine';
  8. import {StacktraceLink} from 'sentry/components/events/interfaces/frame/stacktraceLink';
  9. import {
  10. getLeadHint,
  11. hasAssembly,
  12. hasContextRegisters,
  13. hasContextSource,
  14. hasContextVars,
  15. isExpandable,
  16. trimPackage,
  17. } from 'sentry/components/events/interfaces/frame/utils';
  18. import {formatAddress, parseAddress} from 'sentry/components/events/interfaces/utils';
  19. import {AnnotatedText} from 'sentry/components/events/meta/annotatedText';
  20. import {TraceEventDataSectionContext} from 'sentry/components/events/traceEventDataSection';
  21. import StrictClick from 'sentry/components/strictClick';
  22. import Tag from 'sentry/components/tag';
  23. import {Tooltip} from 'sentry/components/tooltip';
  24. import {SLOW_TOOLTIP_DELAY} from 'sentry/constants';
  25. import {IconChevron} from 'sentry/icons/iconChevron';
  26. import {IconFileBroken} from 'sentry/icons/iconFileBroken';
  27. import {IconRefresh} from 'sentry/icons/iconRefresh';
  28. import {IconWarning} from 'sentry/icons/iconWarning';
  29. import {t, tn} from 'sentry/locale';
  30. import DebugMetaStore from 'sentry/stores/debugMetaStore';
  31. import {space} from 'sentry/styles/space';
  32. import type {
  33. Frame,
  34. PlatformKey,
  35. SentryAppComponent,
  36. SentryAppSchemaStacktraceLink,
  37. } from 'sentry/types';
  38. import type {Event} from 'sentry/types/event';
  39. import {defined} from 'sentry/utils';
  40. import withSentryAppComponents from 'sentry/utils/withSentryAppComponents';
  41. import type DebugImage from './debugMeta/debugImage';
  42. import {combineStatus} from './debugMeta/utils';
  43. import Context from './frame/context';
  44. import {SymbolicatorStatus} from './types';
  45. type Props = {
  46. components: SentryAppComponent<SentryAppSchemaStacktraceLink>[];
  47. event: Event;
  48. frame: Frame;
  49. isUsedForGrouping: boolean;
  50. platform: PlatformKey;
  51. registers: Record<string, string>;
  52. emptySourceNotation?: boolean;
  53. frameMeta?: Record<any, any>;
  54. hiddenFrameCount?: number;
  55. image?: React.ComponentProps<typeof DebugImage>['image'];
  56. includeSystemFrames?: boolean;
  57. isExpanded?: boolean;
  58. isHoverPreviewed?: boolean;
  59. isOnlyFrame?: boolean;
  60. isShowFramesToggleExpanded?: boolean;
  61. /**
  62. * Frames that are hidden under the most recent non-InApp frame
  63. */
  64. isSubFrame?: boolean;
  65. maxLengthOfRelativeAddress?: number;
  66. nextFrame?: Frame;
  67. onShowFramesToggle?: (event: React.MouseEvent<HTMLElement>) => void;
  68. prevFrame?: Frame;
  69. registersMeta?: Record<any, any>;
  70. showStackedFrames?: boolean;
  71. };
  72. function NativeFrame({
  73. frame,
  74. nextFrame,
  75. prevFrame,
  76. includeSystemFrames,
  77. isUsedForGrouping,
  78. maxLengthOfRelativeAddress,
  79. image,
  80. registers,
  81. isOnlyFrame,
  82. event,
  83. components,
  84. hiddenFrameCount,
  85. isShowFramesToggleExpanded,
  86. isSubFrame,
  87. onShowFramesToggle,
  88. isExpanded,
  89. platform,
  90. registersMeta,
  91. frameMeta,
  92. emptySourceNotation = false,
  93. /**
  94. * Is the stack trace being previewed in a hovercard?
  95. */
  96. isHoverPreviewed = false,
  97. }: Props) {
  98. const traceEventDataSectionContext = useContext(TraceEventDataSectionContext);
  99. const absolute = traceEventDataSectionContext?.display.includes('absolute-addresses');
  100. const fullStackTrace = traceEventDataSectionContext?.fullStackTrace;
  101. const fullFunctionName = traceEventDataSectionContext?.display.includes(
  102. 'verbose-function-names'
  103. );
  104. const absoluteFilePaths =
  105. traceEventDataSectionContext?.display.includes('absolute-file-paths');
  106. const tooltipDelay = isHoverPreviewed ? SLOW_TOOLTIP_DELAY : undefined;
  107. const foundByStackScanning = frame.trust === 'scan' || frame.trust === 'cfi-scan';
  108. const startingAddress = image ? image.image_addr : null;
  109. const packageClickable =
  110. !!frame.symbolicatorStatus &&
  111. frame.symbolicatorStatus !== SymbolicatorStatus.UNKNOWN_IMAGE &&
  112. !isHoverPreviewed;
  113. const leadsToApp = !frame.inApp && (nextFrame?.inApp || !nextFrame);
  114. const expandable =
  115. !leadsToApp || includeSystemFrames
  116. ? isExpandable({
  117. frame,
  118. registers,
  119. platform,
  120. emptySourceNotation,
  121. isOnlyFrame,
  122. })
  123. : false;
  124. const inlineFrame =
  125. prevFrame &&
  126. platform === (prevFrame.platform || platform) &&
  127. frame.instructionAddr === prevFrame.instructionAddr;
  128. const functionNameHiddenDetails =
  129. defined(frame.rawFunction) &&
  130. defined(frame.function) &&
  131. frame.function !== frame.rawFunction;
  132. const [expanded, setExpanded] = useState(expandable ? isExpanded ?? false : false);
  133. const [isHovering, setHovering] = useState(false);
  134. const contextLine = (frame?.context || []).find(l => l[0] === frame.lineNo);
  135. const hasStacktraceLink = frame.inApp && !!frame.filename && (isHovering || expanded);
  136. const showSentryAppStacktraceLinkInFrame = hasStacktraceLink && components.length > 0;
  137. const handleMouseEnter = () => setHovering(true);
  138. const handleMouseLeave = () => setHovering(false);
  139. function getRelativeAddress() {
  140. if (!startingAddress) {
  141. return '';
  142. }
  143. const relativeAddress = formatAddress(
  144. parseAddress(frame.instructionAddr) - parseAddress(startingAddress),
  145. maxLengthOfRelativeAddress
  146. );
  147. return `+${relativeAddress}`;
  148. }
  149. function getAddressTooltip() {
  150. if (inlineFrame && foundByStackScanning) {
  151. return t('Inline frame, found by stack scanning');
  152. }
  153. if (inlineFrame) {
  154. return t('Inline frame');
  155. }
  156. if (foundByStackScanning) {
  157. return t('Found by stack scanning');
  158. }
  159. return undefined;
  160. }
  161. function getFunctionName() {
  162. if (functionNameHiddenDetails && fullFunctionName && frame.rawFunction) {
  163. return {
  164. value: frame.rawFunction,
  165. meta: frameMeta?.rawFunction?.[''],
  166. };
  167. }
  168. if (frame.function) {
  169. return {
  170. value: frame.function,
  171. meta: frameMeta?.function?.[''],
  172. };
  173. }
  174. return undefined;
  175. }
  176. // this is the status of image that belongs to this frame
  177. function getStatus() {
  178. // If a matching debug image doesn't exist, fall back to symbolicator_status
  179. if (!image) {
  180. switch (frame.symbolicatorStatus) {
  181. case SymbolicatorStatus.SYMBOLICATED:
  182. return 'success';
  183. case SymbolicatorStatus.MISSING:
  184. case SymbolicatorStatus.MALFORMED:
  185. return 'error';
  186. case SymbolicatorStatus.MISSING_SYMBOL:
  187. case SymbolicatorStatus.UNKNOWN_IMAGE:
  188. default:
  189. return undefined;
  190. }
  191. }
  192. const combinedStatus = combineStatus(image.debug_status, image.unwind_status);
  193. switch (combinedStatus) {
  194. case 'unused':
  195. return undefined;
  196. case 'found':
  197. return 'success';
  198. default:
  199. return 'error';
  200. }
  201. }
  202. function handleGoToImagesLoaded(e: MouseEvent) {
  203. e.stopPropagation(); // to prevent collapsing if collapsible
  204. if (frame.instructionAddr) {
  205. const searchTerm =
  206. !(!frame.addrMode || frame.addrMode === 'abs') && image
  207. ? `${image.debug_id}!${frame.instructionAddr}`
  208. : frame.instructionAddr;
  209. DebugMetaStore.updateFilter(searchTerm);
  210. }
  211. scrollToElement('#images-loaded');
  212. }
  213. function handleToggleContext(e: MouseEvent) {
  214. if (!expandable) {
  215. return;
  216. }
  217. e.preventDefault();
  218. setExpanded(!expanded);
  219. }
  220. const relativeAddress = getRelativeAddress();
  221. const addressTooltip = getAddressTooltip();
  222. const functionName = getFunctionName();
  223. const status = getStatus();
  224. return (
  225. <StackTraceFrame data-test-id="stack-trace-frame">
  226. <StrictClick onClick={handleToggleContext}>
  227. <RowHeader
  228. expandable={expandable}
  229. expanded={expanded}
  230. isInAppFrame={frame.inApp}
  231. isSubFrame={!!isSubFrame}
  232. onMouseEnter={handleMouseEnter}
  233. onMouseLeave={handleMouseLeave}
  234. >
  235. <SymbolicatorIcon>
  236. {status === 'error' ? (
  237. <Tooltip
  238. title={t(
  239. 'This frame has missing debug files and could not be symbolicated'
  240. )}
  241. >
  242. <IconFileBroken
  243. size="sm"
  244. color="errorText"
  245. data-test-id="symbolication-error-icon"
  246. />
  247. </Tooltip>
  248. ) : status === undefined ? (
  249. <Tooltip
  250. title={t(
  251. 'This frame has an unknown problem and could not be symbolicated'
  252. )}
  253. >
  254. <IconWarning
  255. size="sm"
  256. color="warningText"
  257. data-test-id="symbolication-warning-icon"
  258. />
  259. </Tooltip>
  260. ) : null}
  261. </SymbolicatorIcon>
  262. <div>
  263. {!fullStackTrace && !expanded && leadsToApp && (
  264. <Fragment>
  265. <PackageNote>
  266. {getLeadHint({event, hasNextFrame: defined(nextFrame)})}
  267. </PackageNote>
  268. </Fragment>
  269. )}
  270. <Tooltip
  271. title={frame.package ?? t('Go to images loaded')}
  272. position="bottom"
  273. containerDisplayMode="inline-flex"
  274. delay={tooltipDelay}
  275. >
  276. <Package>
  277. {frame.package ? trimPackage(frame.package) : `<${t('unknown')}>`}
  278. </Package>
  279. </Tooltip>
  280. </div>
  281. <GenericCellWrapper>
  282. <AddressCell onClick={packageClickable ? handleGoToImagesLoaded : undefined}>
  283. <Tooltip
  284. title={addressTooltip}
  285. disabled={!(foundByStackScanning || inlineFrame)}
  286. delay={tooltipDelay}
  287. >
  288. {!relativeAddress || absolute ? frame.instructionAddr : relativeAddress}
  289. </Tooltip>
  290. </AddressCell>
  291. </GenericCellWrapper>
  292. <FunctionNameCell>
  293. {functionName ? (
  294. <AnnotatedText value={functionName.value} meta={functionName.meta} />
  295. ) : (
  296. `<${t('unknown')}>`
  297. )}{' '}
  298. {frame.filename && (
  299. <Tooltip
  300. title={frame.absPath}
  301. disabled={!(defined(frame.absPath) && frame.absPath !== frame.filename)}
  302. delay={tooltipDelay}
  303. isHoverable
  304. >
  305. <FileName>
  306. {'('}
  307. {absoluteFilePaths ? frame.absPath : frame.filename}
  308. {frame.lineNo && `:${frame.lineNo}`}
  309. {')'}
  310. </FileName>
  311. </Tooltip>
  312. )}
  313. </FunctionNameCell>
  314. <GroupingCell>
  315. {isUsedForGrouping && (
  316. <Tooltip title={t('This frame is repeated in every event of this issue')}>
  317. <IconRefresh size="sm" color="textColor" />
  318. </Tooltip>
  319. )}
  320. </GroupingCell>
  321. {hiddenFrameCount ? (
  322. <ShowHideButton
  323. analyticsEventName="Stacktrace Frames: toggled"
  324. analyticsEventKey="stacktrace_frames.toggled"
  325. analyticsParams={{
  326. frame_count: hiddenFrameCount,
  327. is_frame_expanded: isShowFramesToggleExpanded,
  328. }}
  329. size="xs"
  330. borderless
  331. onClick={e => {
  332. onShowFramesToggle?.(e);
  333. }}
  334. >
  335. {isShowFramesToggleExpanded
  336. ? tn('Hide %s more frame', 'Hide %s more frames', hiddenFrameCount)
  337. : tn('Show %s more frame', 'Show %s more frames', hiddenFrameCount)}
  338. </ShowHideButton>
  339. ) : null}
  340. <GenericCellWrapper>
  341. {hasStacktraceLink && (
  342. <ErrorBoundary>
  343. <StacktraceLink
  344. frame={frame}
  345. line={contextLine ? contextLine[1] : ''}
  346. event={event}
  347. />
  348. </ErrorBoundary>
  349. )}
  350. {showSentryAppStacktraceLinkInFrame && (
  351. <ErrorBoundary mini>
  352. <OpenInContextLine
  353. lineNo={frame.lineNo}
  354. filename={frame.filename || ''}
  355. components={components}
  356. />
  357. </ErrorBoundary>
  358. )}
  359. <TypeCell>
  360. {frame.inApp ? <Tag type="info">{t('In App')}</Tag> : null}
  361. </TypeCell>
  362. </GenericCellWrapper>
  363. <ExpandCell>
  364. {expandable && (
  365. <ToggleButton
  366. size="zero"
  367. aria-label={t('Toggle Context')}
  368. tooltipProps={isHoverPreviewed ? {delay: SLOW_TOOLTIP_DELAY} : undefined}
  369. icon={
  370. <IconChevron legacySize="8px" direction={expanded ? 'up' : 'down'} />
  371. }
  372. />
  373. )}
  374. </ExpandCell>
  375. </RowHeader>
  376. </StrictClick>
  377. {expanded && (
  378. <Registers
  379. frame={frame}
  380. event={event}
  381. registers={registers}
  382. components={components}
  383. hasContextSource={hasContextSource(frame)}
  384. hasContextVars={hasContextVars(frame)}
  385. hasContextRegisters={hasContextRegisters(registers)}
  386. emptySourceNotation={emptySourceNotation}
  387. hasAssembly={hasAssembly(frame, platform)}
  388. isExpanded={expanded}
  389. registersMeta={registersMeta}
  390. frameMeta={frameMeta}
  391. />
  392. )}
  393. </StackTraceFrame>
  394. );
  395. }
  396. export default withSentryAppComponents(NativeFrame, {componentType: 'stacktrace-link'});
  397. const GenericCellWrapper = styled('div')`
  398. display: flex;
  399. `;
  400. const AddressCell = styled('div')`
  401. font-family: ${p => p.theme.text.familyMono};
  402. ${p => p.onClick && `cursor: pointer`};
  403. ${p => p.onClick && `color:` + p.theme.linkColor};
  404. `;
  405. const FunctionNameCell = styled('div')`
  406. word-break: break-all;
  407. @media (max-width: ${p => p.theme.breakpoints.small}) {
  408. grid-column: 2/6;
  409. }
  410. `;
  411. const GroupingCell = styled('div')`
  412. @media (max-width: ${p => p.theme.breakpoints.small}) {
  413. grid-row: 2/3;
  414. }
  415. `;
  416. const TypeCell = styled('div')`
  417. @media (max-width: ${p => p.theme.breakpoints.small}) {
  418. grid-column: 5/6;
  419. grid-row: 1/2;
  420. }
  421. `;
  422. const ExpandCell = styled('div')`
  423. @media (max-width: ${p => p.theme.breakpoints.small}) {
  424. grid-column: 6/7;
  425. grid-row: 1/2;
  426. }
  427. `;
  428. const ToggleButton = styled(Button)`
  429. width: 16px;
  430. height: 16px;
  431. `;
  432. const Registers = styled(Context)`
  433. border-bottom: 1px solid ${p => p.theme.border};
  434. padding: 0;
  435. margin: 0;
  436. `;
  437. const PackageNote = styled('div')`
  438. color: ${p => p.theme.subText};
  439. font-size: ${p => p.theme.fontSizeExtraSmall};
  440. `;
  441. const Package = styled('span')`
  442. white-space: nowrap;
  443. overflow: hidden;
  444. text-overflow: ellipsis;
  445. width: 100%;
  446. padding-right: 2px; /* Needed to prevent text cropping with italic font */
  447. `;
  448. const FileName = styled('span')`
  449. color: ${p => p.theme.subText};
  450. border-bottom: 1px dashed ${p => p.theme.border};
  451. `;
  452. const RowHeader = styled('span')<{
  453. expandable: boolean;
  454. expanded: boolean;
  455. isInAppFrame: boolean;
  456. isSubFrame: boolean;
  457. }>`
  458. display: grid;
  459. grid-template-columns: repeat(2, auto) 1fr repeat(2, auto) ${space(2)};
  460. grid-template-rows: repeat(2, auto);
  461. align-items: center;
  462. align-content: center;
  463. column-gap: ${space(1)};
  464. background-color: ${p =>
  465. !p.isInAppFrame && p.isSubFrame
  466. ? `${p.theme.surface100}`
  467. : `${p.theme.bodyBackground}`};
  468. font-size: ${p => p.theme.fontSizeSmall};
  469. padding: ${space(1)};
  470. color: ${p => (!p.isInAppFrame ? p.theme.subText : '')};
  471. font-style: ${p => (!p.isInAppFrame ? 'italic' : '')};
  472. ${p => p.expandable && `cursor: pointer;`};
  473. @media (min-width: ${p => p.theme.breakpoints.small}) {
  474. grid-template-columns: auto 150px 120px 4fr repeat(2, auto) ${space(2)};
  475. padding: ${space(0.5)} ${space(1.5)};
  476. min-height: 32px;
  477. }
  478. `;
  479. const StackTraceFrame = styled('li')`
  480. :not(:last-child) {
  481. ${RowHeader} {
  482. border-bottom: 1px solid ${p => p.theme.border};
  483. }
  484. }
  485. &:last-child {
  486. ${RowHeader} {
  487. border-bottom-right-radius: 5px;
  488. border-bottom-left-radius: 5px;
  489. }
  490. }
  491. &:first-child {
  492. ${RowHeader} {
  493. border-top-right-radius: 5px;
  494. }
  495. }
  496. `;
  497. const SymbolicatorIcon = styled('div')`
  498. width: ${p => p.theme.iconSizes.sm};
  499. `;
  500. const ShowHideButton = styled(Button)`
  501. color: ${p => p.theme.subText};
  502. font-style: italic;
  503. font-weight: normal;
  504. padding: ${space(0.25)} ${space(0.5)};
  505. &:hover {
  506. color: ${p => p.theme.subText};
  507. }
  508. `;