transactionDetail.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import omit from 'lodash/omit';
  5. import {LinkButton} from 'sentry/components/button';
  6. import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
  7. import {Alert} from 'sentry/components/core/alert';
  8. import {DateTime} from 'sentry/components/dateTime';
  9. import {getFormattedTimeRangeWithLeadingAndTrailingZero} from 'sentry/components/events/interfaces/spans/utils';
  10. import Link from 'sentry/components/links/link';
  11. import {
  12. ErrorDot,
  13. ErrorLevel,
  14. ErrorMessageContent,
  15. ErrorMessageTitle,
  16. ErrorTitle,
  17. } from 'sentry/components/performance/waterfall/rowDetails';
  18. import {generateIssueEventTarget} from 'sentry/components/quickTrace/utils';
  19. import {PAGE_URL_PARAM} from 'sentry/constants/pageFilters';
  20. import {t, tn} from 'sentry/locale';
  21. import {space} from 'sentry/styles/space';
  22. import type {Organization} from 'sentry/types/organization';
  23. import {trackAnalytics} from 'sentry/utils/analytics';
  24. import {browserHistory} from 'sentry/utils/browserHistory';
  25. import {generateEventSlug} from 'sentry/utils/discover/urls';
  26. import getDynamicText from 'sentry/utils/getDynamicText';
  27. import type {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
  28. import {getTransactionDetailsUrl} from 'sentry/utils/performance/urls';
  29. import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
  30. import {CustomProfiler} from 'sentry/utils/performanceForSentry';
  31. import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
  32. import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
  33. import {Row, Tags, TransactionDetails, TransactionDetailsContainer} from './styles';
  34. type Props = {
  35. location: Location;
  36. organization: Organization;
  37. scrollIntoView: () => void;
  38. transaction: TraceFullDetailed;
  39. };
  40. class TransactionDetail extends Component<Props> {
  41. componentDidMount() {
  42. const {organization, transaction} = this.props;
  43. trackAnalytics('performance_views.trace_view.open_transaction_details', {
  44. organization,
  45. operation: transaction['transaction.op'],
  46. transaction: transaction.transaction,
  47. });
  48. }
  49. renderTransactionErrors() {
  50. const {organization, transaction} = this.props;
  51. const {errors, performance_issues} = transaction;
  52. if (errors.length + performance_issues.length === 0) {
  53. return null;
  54. }
  55. return (
  56. <Alert.Container>
  57. <Alert
  58. system
  59. type="error"
  60. expand={[...errors, ...performance_issues].map(error => (
  61. <ErrorMessageContent key={error.event_id}>
  62. <ErrorDot level={error.level} />
  63. <ErrorLevel>{error.level}</ErrorLevel>
  64. <ErrorTitle>
  65. <Link to={generateIssueEventTarget(error, organization)}>
  66. {error.title}
  67. </Link>
  68. </ErrorTitle>
  69. </ErrorMessageContent>
  70. ))}
  71. >
  72. <ErrorMessageTitle>
  73. {tn(
  74. '%s issue occurred in this transaction.',
  75. '%s issues occurred in this transaction.',
  76. errors.length + performance_issues.length
  77. )}
  78. </ErrorMessageTitle>
  79. </Alert>
  80. </Alert.Container>
  81. );
  82. }
  83. renderGoToTransactionButton() {
  84. const {location, organization, transaction} = this.props;
  85. const eventSlug = generateEventSlug({
  86. id: transaction.event_id,
  87. project: transaction.project_slug,
  88. });
  89. const target = getTransactionDetailsUrl(
  90. organization.slug,
  91. eventSlug,
  92. transaction.transaction,
  93. omit(location.query, Object.values(PAGE_URL_PARAM))
  94. );
  95. return (
  96. <StyledLinkButton size="xs" to={target}>
  97. {t('View Event')}
  98. </StyledLinkButton>
  99. );
  100. }
  101. renderGoToSummaryButton() {
  102. const {location, organization, transaction} = this.props;
  103. const target = transactionSummaryRouteWithQuery({
  104. organization,
  105. transaction: transaction.transaction,
  106. query: omit(location.query, Object.values(PAGE_URL_PARAM)),
  107. projectID: String(transaction.project_id),
  108. });
  109. return (
  110. <StyledLinkButton size="xs" to={target}>
  111. {t('View Summary')}
  112. </StyledLinkButton>
  113. );
  114. }
  115. renderGoToProfileButton() {
  116. const {organization, transaction} = this.props;
  117. if (!transaction.profile_id) {
  118. return null;
  119. }
  120. const target = generateProfileFlamechartRoute({
  121. organization,
  122. projectSlug: transaction.project_slug,
  123. profileId: transaction.profile_id,
  124. });
  125. function handleOnClick() {
  126. trackAnalytics('profiling_views.go_to_flamegraph', {
  127. organization,
  128. source: 'performance.trace_view',
  129. });
  130. }
  131. return (
  132. <StyledLinkButton size="xs" to={target} onClick={handleOnClick}>
  133. {t('View Profile')}
  134. </StyledLinkButton>
  135. );
  136. }
  137. renderMeasurements() {
  138. const {transaction} = this.props;
  139. const {measurements = {}} = transaction;
  140. const measurementKeys = Object.keys(measurements)
  141. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  142. .filter(name => Boolean(WEB_VITAL_DETAILS[`measurements.${name}`]))
  143. .sort();
  144. if (measurementKeys.length <= 0) {
  145. return null;
  146. }
  147. return (
  148. <Fragment>
  149. {measurementKeys.map(measurement => (
  150. <Row
  151. key={measurement}
  152. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  153. title={WEB_VITAL_DETAILS[`measurements.${measurement}`]?.name}
  154. >
  155. {`${Number(measurements[measurement]!.value.toFixed(3)).toLocaleString()}ms`}
  156. </Row>
  157. ))}
  158. </Fragment>
  159. );
  160. }
  161. scrollBarIntoView =
  162. (transactionId: string) => (e: React.MouseEvent<HTMLAnchorElement>) => {
  163. // do not use the default anchor behaviour
  164. // because it will be hidden behind the minimap
  165. e.preventDefault();
  166. const hash = `#txn-${transactionId}`;
  167. this.props.scrollIntoView();
  168. // TODO(txiao): This is causing a rerender of the whole page,
  169. // which can be slow.
  170. //
  171. // make sure to update the location
  172. browserHistory.push({
  173. ...this.props.location,
  174. hash,
  175. });
  176. };
  177. renderTransactionDetail() {
  178. const {location, organization, transaction} = this.props;
  179. const startTimestamp = Math.min(transaction.start_timestamp, transaction.timestamp);
  180. const endTimestamp = Math.max(transaction.start_timestamp, transaction.timestamp);
  181. const {start: startTimeWithLeadingZero, end: endTimeWithLeadingZero} =
  182. getFormattedTimeRangeWithLeadingAndTrailingZero(startTimestamp, endTimestamp);
  183. const duration = (endTimestamp - startTimestamp) * 1000;
  184. const durationString = `${Number(duration.toFixed(3)).toLocaleString()}ms`;
  185. return (
  186. <TransactionDetails>
  187. <table className="table key-value">
  188. <tbody>
  189. <Row
  190. title={
  191. <TransactionIdTitle
  192. onClick={this.scrollBarIntoView(transaction.event_id)}
  193. >
  194. {t('Event ID')}
  195. </TransactionIdTitle>
  196. }
  197. extra={this.renderGoToTransactionButton()}
  198. >
  199. {transaction.event_id}
  200. <CopyToClipboardButton
  201. borderless
  202. size="zero"
  203. iconSize="xs"
  204. text={`${window.location.href.replace(window.location.hash, '')}#txn-${
  205. transaction.event_id
  206. }`}
  207. />
  208. </Row>
  209. <Row title="Transaction" extra={this.renderGoToSummaryButton()}>
  210. {transaction.transaction}
  211. </Row>
  212. <Row title="Transaction Status">{transaction['transaction.status']}</Row>
  213. <Row title="Span ID">{transaction.span_id}</Row>
  214. {transaction.profile_id && (
  215. <Row title="Profile ID" extra={this.renderGoToProfileButton()}>
  216. {transaction.profile_id}
  217. </Row>
  218. )}
  219. <Row title="Project">{transaction.project_slug}</Row>
  220. <Row title="Start Date">
  221. {getDynamicText({
  222. fixed: 'Mar 19, 2021 11:06:27 AM UTC',
  223. value: (
  224. <Fragment>
  225. <DateTime date={startTimestamp * 1000} />
  226. {` (${startTimeWithLeadingZero})`}
  227. </Fragment>
  228. ),
  229. })}
  230. </Row>
  231. <Row title="End Date">
  232. {getDynamicText({
  233. fixed: 'Mar 19, 2021 11:06:28 AM UTC',
  234. value: (
  235. <Fragment>
  236. <DateTime date={endTimestamp * 1000} />
  237. {` (${endTimeWithLeadingZero})`}
  238. </Fragment>
  239. ),
  240. })}
  241. </Row>
  242. <Row title="Duration">{durationString}</Row>
  243. <Row title="Operation">{transaction['transaction.op'] || ''}</Row>
  244. {this.renderMeasurements()}
  245. <Tags
  246. location={location}
  247. organization={organization}
  248. tags={transaction.tags ?? []}
  249. event={transaction}
  250. />
  251. </tbody>
  252. </table>
  253. </TransactionDetails>
  254. );
  255. }
  256. render() {
  257. return (
  258. <CustomProfiler id="TransactionDetail">
  259. <TransactionDetailsContainer
  260. onClick={event => {
  261. // prevent toggling the transaction detail
  262. event.stopPropagation();
  263. }}
  264. >
  265. {this.renderTransactionErrors()}
  266. {this.renderTransactionDetail()}
  267. </TransactionDetailsContainer>
  268. </CustomProfiler>
  269. );
  270. }
  271. }
  272. const TransactionIdTitle = styled('a')`
  273. display: flex;
  274. color: ${p => p.theme.textColor};
  275. :hover {
  276. color: ${p => p.theme.textColor};
  277. }
  278. `;
  279. const StyledLinkButton = styled(LinkButton)`
  280. position: absolute;
  281. top: ${space(0.75)};
  282. right: ${space(0.5)};
  283. `;
  284. export default TransactionDetail;