deprecatedLine.tsx 17 KB

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