line.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import classNames from 'classnames';
  4. import scrollToElement from 'scroll-to-element';
  5. import {Button} from 'sentry/components/button';
  6. import {
  7. StacktraceFilenameQuery,
  8. useSourceMapDebug,
  9. } from 'sentry/components/events/interfaces/crashContent/exception/useSourceMapDebug';
  10. import StrictClick from 'sentry/components/strictClick';
  11. import {Tooltip} from 'sentry/components/tooltip';
  12. import {SLOW_TOOLTIP_DELAY} from 'sentry/constants';
  13. import {IconChevron, IconRefresh, IconWarning} from 'sentry/icons';
  14. import {t} from 'sentry/locale';
  15. import DebugMetaStore from 'sentry/stores/debugMetaStore';
  16. import {space} from 'sentry/styles/space';
  17. import {Frame, Organization, PlatformType, SentryAppComponent} from 'sentry/types';
  18. import {Event} from 'sentry/types/event';
  19. import withOrganization from 'sentry/utils/withOrganization';
  20. import withSentryAppComponents from 'sentry/utils/withSentryAppComponents';
  21. import DebugImage from '../debugMeta/debugImage';
  22. import {combineStatus} from '../debugMeta/utils';
  23. import {SymbolicatorStatus} from '../types';
  24. import {CodecovLegend} from './codecovLegend';
  25. import Context from './context';
  26. import DefaultTitle from './defaultTitle';
  27. import PackageLink from './packageLink';
  28. import PackageStatus, {PackageStatusIcon} from './packageStatus';
  29. import Symbol, {FunctionNameToggleIcon} from './symbol';
  30. import TogglableAddress, {AddressToggleIcon} from './togglableAddress';
  31. import {
  32. getPlatform,
  33. hasAssembly,
  34. hasContextRegisters,
  35. hasContextSource,
  36. hasContextVars,
  37. isDotnet,
  38. isExpandable,
  39. } from './utils';
  40. type Props = {
  41. components: Array<SentryAppComponent>;
  42. data: Frame;
  43. event: Event;
  44. registers: Record<string, string>;
  45. debugFrames?: StacktraceFilenameQuery[];
  46. emptySourceNotation?: boolean;
  47. frameMeta?: Record<any, any>;
  48. image?: React.ComponentProps<typeof DebugImage>['image'];
  49. includeSystemFrames?: boolean;
  50. isExpanded?: boolean;
  51. isFirst?: boolean;
  52. isFrameAfterLastNonApp?: boolean;
  53. /**
  54. * Is the stack trace being previewed in a hovercard?
  55. */
  56. isHoverPreviewed?: boolean;
  57. isOnlyFrame?: boolean;
  58. maxLengthOfRelativeAddress?: number;
  59. nextFrame?: Frame;
  60. onAddressToggle?: (event: React.MouseEvent<SVGElement>) => void;
  61. onFunctionNameToggle?: (event: React.MouseEvent<SVGElement>) => void;
  62. organization?: Organization;
  63. platform?: PlatformType;
  64. prevFrame?: Frame;
  65. registersMeta?: Record<any, any>;
  66. showCompleteFunctionName?: boolean;
  67. showingAbsoluteAddress?: boolean;
  68. timesRepeated?: number;
  69. };
  70. type State = {
  71. isExpanded?: boolean;
  72. };
  73. function makeFilter(
  74. addr: string,
  75. addrMode: string | undefined,
  76. image?: React.ComponentProps<typeof DebugImage>['image']
  77. ): string {
  78. if (!(!addrMode || addrMode === 'abs') && image) {
  79. return `${image.debug_id}!${addr}`;
  80. }
  81. return addr;
  82. }
  83. function SourceMapWarning({
  84. debugFrames,
  85. frame,
  86. }: {
  87. frame: Frame;
  88. debugFrames?: StacktraceFilenameQuery[];
  89. }) {
  90. const debugFrame = debugFrames?.find(debug => debug.filename === frame.filename);
  91. const {data} = useSourceMapDebug(debugFrame?.query, {
  92. enabled: !!debugFrame,
  93. });
  94. return data?.errors?.length ? (
  95. <IconWrapper>
  96. <Tooltip skipWrapper title={t('Missing source map')}>
  97. <IconWarning color="red400" size="sm" aria-label={t('Missing source map')} />
  98. </Tooltip>
  99. </IconWrapper>
  100. ) : null;
  101. }
  102. export class Line extends Component<Props, State> {
  103. static defaultProps = {
  104. isExpanded: false,
  105. emptySourceNotation: false,
  106. isHoverPreviewed: false,
  107. };
  108. // isExpanded can be initialized to true via parent component;
  109. // data synchronization is not important
  110. // https://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html
  111. state: State = {
  112. isExpanded: this.props.isExpanded,
  113. };
  114. toggleContext = evt => {
  115. evt && evt.preventDefault();
  116. this.setState({
  117. isExpanded: !this.state.isExpanded,
  118. });
  119. };
  120. getPlatform() {
  121. // prioritize the frame platform but fall back to the platform
  122. // of the stack trace / exception
  123. return getPlatform(this.props.data.platform, this.props.platform ?? 'other');
  124. }
  125. isInlineFrame() {
  126. return (
  127. this.props.prevFrame &&
  128. this.getPlatform() === (this.props.prevFrame.platform || this.props.platform) &&
  129. this.props.data.instructionAddr === this.props.prevFrame.instructionAddr
  130. );
  131. }
  132. isExpandable() {
  133. const {registers, platform, emptySourceNotation, isOnlyFrame, data} = this.props;
  134. return isExpandable({
  135. frame: data,
  136. registers,
  137. platform,
  138. emptySourceNotation,
  139. isOnlyFrame,
  140. });
  141. }
  142. shouldShowLinkToImage() {
  143. const {isHoverPreviewed, data} = this.props;
  144. const {symbolicatorStatus} = data;
  145. return (
  146. !!symbolicatorStatus &&
  147. symbolicatorStatus !== SymbolicatorStatus.UNKNOWN_IMAGE &&
  148. !isHoverPreviewed
  149. );
  150. }
  151. packageStatus() {
  152. // this is the status of image that belongs to this frame
  153. const {image} = this.props;
  154. if (!image) {
  155. return 'empty';
  156. }
  157. const combinedStatus = combineStatus(image.debug_status, image.unwind_status);
  158. switch (combinedStatus) {
  159. case 'unused':
  160. return 'empty';
  161. case 'found':
  162. return 'success';
  163. default:
  164. return 'error';
  165. }
  166. }
  167. scrollToImage = event => {
  168. event.stopPropagation(); // to prevent collapsing if collapsible
  169. const {instructionAddr, addrMode} = this.props.data;
  170. if (instructionAddr) {
  171. DebugMetaStore.updateFilter(
  172. makeFilter(instructionAddr, addrMode, this.props.image)
  173. );
  174. }
  175. scrollToElement('#images-loaded');
  176. };
  177. preventCollapse = evt => {
  178. evt.stopPropagation();
  179. };
  180. renderExpander() {
  181. if (!this.isExpandable()) {
  182. return null;
  183. }
  184. const {isHoverPreviewed} = this.props;
  185. const {isExpanded} = this.state;
  186. return (
  187. <ToggleContextButtonWrapper>
  188. <ToggleContextButton
  189. className="btn-toggle"
  190. data-test-id={`toggle-button-${isExpanded ? 'expanded' : 'collapsed'}`}
  191. css={isDotnet(this.getPlatform()) && {display: 'block !important'}} // remove important once we get rid of css files
  192. size="zero"
  193. title={t('Toggle Context')}
  194. tooltipProps={isHoverPreviewed ? {delay: SLOW_TOOLTIP_DELAY} : undefined}
  195. onClick={this.toggleContext}
  196. >
  197. <IconChevron direction={isExpanded ? 'up' : 'down'} legacySize="8px" />
  198. </ToggleContextButton>
  199. </ToggleContextButtonWrapper>
  200. );
  201. }
  202. leadsToApp() {
  203. const {data, nextFrame} = this.props;
  204. return !data.inApp && ((nextFrame && nextFrame.inApp) || !nextFrame);
  205. }
  206. isFoundByStackScanning() {
  207. const {data} = this.props;
  208. return data.trust === 'scan' || data.trust === 'cfi-scan';
  209. }
  210. renderLeadHint() {
  211. const {isExpanded} = this.state;
  212. if (isExpanded) {
  213. return null;
  214. }
  215. const leadsToApp = this.leadsToApp();
  216. if (!leadsToApp) {
  217. return null;
  218. }
  219. const {nextFrame} = this.props;
  220. return !nextFrame ? (
  221. <LeadHint className="leads-to-app-hint" width="115px">
  222. {t('Crashed in non-app')}
  223. {': '}
  224. </LeadHint>
  225. ) : (
  226. <LeadHint className="leads-to-app-hint">
  227. {t('Called from')}
  228. {': '}
  229. </LeadHint>
  230. );
  231. }
  232. renderRepeats() {
  233. const timesRepeated = this.props.timesRepeated;
  234. if (timesRepeated && timesRepeated > 0) {
  235. return (
  236. <RepeatedFrames
  237. title={`Frame repeated ${timesRepeated} time${timesRepeated === 1 ? '' : 's'}`}
  238. >
  239. <RepeatedContent>
  240. <StyledIconRefresh />
  241. <span>{timesRepeated}</span>
  242. </RepeatedContent>
  243. </RepeatedFrames>
  244. );
  245. }
  246. return null;
  247. }
  248. renderDefaultLine() {
  249. const {isHoverPreviewed, debugFrames, data} = this.props;
  250. return (
  251. <StrictClick onClick={this.isExpandable() ? this.toggleContext : undefined}>
  252. <DefaultLine className="title" data-test-id="title">
  253. <VertCenterWrapper>
  254. <SourceMapWarning frame={data} debugFrames={debugFrames} />
  255. <div>
  256. {this.renderLeadHint()}
  257. <DefaultTitle
  258. frame={data}
  259. platform={this.props.platform ?? 'other'}
  260. isHoverPreviewed={isHoverPreviewed}
  261. meta={this.props.frameMeta}
  262. />
  263. </div>
  264. {this.renderRepeats()}
  265. </VertCenterWrapper>
  266. {this.renderExpander()}
  267. </DefaultLine>
  268. </StrictClick>
  269. );
  270. }
  271. renderNativeLine() {
  272. const {
  273. data,
  274. showingAbsoluteAddress,
  275. onAddressToggle,
  276. onFunctionNameToggle,
  277. image,
  278. maxLengthOfRelativeAddress,
  279. includeSystemFrames,
  280. isFrameAfterLastNonApp,
  281. showCompleteFunctionName,
  282. isHoverPreviewed,
  283. } = this.props;
  284. const leadHint = this.renderLeadHint();
  285. const packageStatus = this.packageStatus();
  286. return (
  287. <StrictClick onClick={this.isExpandable() ? this.toggleContext : undefined}>
  288. <DefaultLine className="title as-table" data-test-id="title">
  289. <NativeLineContent isFrameAfterLastNonApp={!!isFrameAfterLastNonApp}>
  290. <PackageInfo>
  291. {leadHint}
  292. <PackageLink
  293. includeSystemFrames={!!includeSystemFrames}
  294. withLeadHint={leadHint !== null}
  295. packagePath={data.package}
  296. onClick={this.scrollToImage}
  297. isClickable={this.shouldShowLinkToImage()}
  298. isHoverPreviewed={isHoverPreviewed}
  299. >
  300. {!isHoverPreviewed && (
  301. <PackageStatus
  302. status={packageStatus}
  303. tooltip={t('Go to Images Loaded')}
  304. />
  305. )}
  306. </PackageLink>
  307. </PackageInfo>
  308. {data.instructionAddr && (
  309. <TogglableAddress
  310. address={data.instructionAddr}
  311. startingAddress={image ? image.image_addr ?? null : null}
  312. isAbsolute={!!showingAbsoluteAddress}
  313. isFoundByStackScanning={this.isFoundByStackScanning()}
  314. isInlineFrame={!!this.isInlineFrame()}
  315. onToggle={onAddressToggle}
  316. relativeAddressMaxlength={maxLengthOfRelativeAddress}
  317. isHoverPreviewed={isHoverPreviewed}
  318. />
  319. )}
  320. <Symbol
  321. frame={data}
  322. showCompleteFunctionName={!!showCompleteFunctionName}
  323. onFunctionNameToggle={onFunctionNameToggle}
  324. isHoverPreviewed={isHoverPreviewed}
  325. />
  326. </NativeLineContent>
  327. {this.renderExpander()}
  328. </DefaultLine>
  329. </StrictClick>
  330. );
  331. }
  332. renderLine() {
  333. switch (this.getPlatform()) {
  334. case 'objc':
  335. // fallthrough
  336. case 'cocoa':
  337. // fallthrough
  338. case 'native':
  339. return this.renderNativeLine();
  340. default:
  341. return this.renderDefaultLine();
  342. }
  343. }
  344. render() {
  345. const data = this.props.data;
  346. const className = classNames({
  347. frame: true,
  348. 'is-expandable': this.isExpandable(),
  349. expanded: this.state.isExpanded,
  350. collapsed: !this.state.isExpanded,
  351. 'system-frame': !data.inApp,
  352. 'frame-errors': data.errors,
  353. 'leads-to-app': this.leadsToApp(),
  354. });
  355. const props = {className};
  356. const shouldShowCodecovLegend =
  357. this.props.organization?.features.includes('codecov-stacktrace-integration') &&
  358. this.props.organization?.codecovAccess &&
  359. !this.props.nextFrame &&
  360. this.state.isExpanded;
  361. return (
  362. <StyledLi data-test-id="line" {...props}>
  363. {shouldShowCodecovLegend && (
  364. <CodecovLegend
  365. event={this.props.event}
  366. frame={this.props.data}
  367. organization={this.props.organization}
  368. />
  369. )}
  370. {this.renderLine()}
  371. <Context
  372. frame={data}
  373. event={this.props.event}
  374. registers={this.props.registers}
  375. components={this.props.components}
  376. hasContextSource={hasContextSource(data)}
  377. hasContextVars={hasContextVars(data)}
  378. hasContextRegisters={hasContextRegisters(this.props.registers)}
  379. emptySourceNotation={this.props.emptySourceNotation}
  380. hasAssembly={hasAssembly(data, this.props.platform)}
  381. expandable={this.isExpandable()}
  382. isExpanded={this.state.isExpanded}
  383. registersMeta={this.props.registersMeta}
  384. frameMeta={this.props.frameMeta}
  385. />
  386. </StyledLi>
  387. );
  388. }
  389. }
  390. export default withOrganization(
  391. withSentryAppComponents(Line, {componentType: 'stacktrace-link'})
  392. );
  393. const PackageInfo = styled('div')`
  394. display: grid;
  395. grid-template-columns: auto 1fr;
  396. order: 2;
  397. align-items: flex-start;
  398. @media (min-width: ${props => props.theme.breakpoints.small}) {
  399. order: 0;
  400. }
  401. `;
  402. const RepeatedFrames = styled('div')`
  403. display: inline-block;
  404. `;
  405. const VertCenterWrapper = styled('div')`
  406. display: flex;
  407. align-items: center;
  408. justify-content: space-between;
  409. `;
  410. const RepeatedContent = styled(VertCenterWrapper)`
  411. justify-content: center;
  412. `;
  413. const NativeLineContent = styled('div')<{isFrameAfterLastNonApp: boolean}>`
  414. display: grid;
  415. flex: 1;
  416. gap: ${space(0.5)};
  417. grid-template-columns: ${p =>
  418. `minmax(${p.isFrameAfterLastNonApp ? '167px' : '117px'}, auto) 1fr`};
  419. align-items: center;
  420. justify-content: flex-start;
  421. @media (min-width: ${props => props.theme.breakpoints.small}) {
  422. grid-template-columns:
  423. ${p => (p.isFrameAfterLastNonApp ? '200px' : '150px')} minmax(117px, auto)
  424. 1fr;
  425. }
  426. @media (min-width: ${props => props.theme.breakpoints.large}) and (max-width: ${props =>
  427. props.theme.breakpoints.xlarge}) {
  428. grid-template-columns:
  429. ${p => (p.isFrameAfterLastNonApp ? '180px' : '140px')} minmax(117px, auto)
  430. 1fr;
  431. }
  432. `;
  433. const DefaultLine = styled('div')`
  434. display: grid;
  435. grid-template-columns: 1fr auto;
  436. align-items: center;
  437. `;
  438. const StyledIconRefresh = styled(IconRefresh)`
  439. margin-right: ${space(0.25)};
  440. `;
  441. const LeadHint = styled('div')<{width?: string}>`
  442. ${p => p.theme.overflowEllipsis}
  443. max-width: ${p => (p.width ? p.width : '67px')}
  444. `;
  445. const ToggleContextButtonWrapper = styled('span')`
  446. margin-left: ${space(1)};
  447. `;
  448. // the Button's label has the padding of 3px because the button size has to be 16x16 px.
  449. const ToggleContextButton = styled(Button)`
  450. span:first-child {
  451. padding: 3px;
  452. }
  453. `;
  454. const StyledLi = styled('li')`
  455. ${PackageStatusIcon} {
  456. flex-shrink: 0;
  457. }
  458. :hover {
  459. ${PackageStatusIcon} {
  460. visibility: visible;
  461. }
  462. ${AddressToggleIcon} {
  463. visibility: visible;
  464. }
  465. ${FunctionNameToggleIcon} {
  466. visibility: visible;
  467. }
  468. }
  469. `;
  470. const IconWrapper = styled('div')`
  471. display: flex;
  472. align-items: center;
  473. margin-right: ${space(1)};
  474. `;