transactionDetail.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import {Component, Fragment} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import omit from 'lodash/omit';
  6. import Alert from 'sentry/components/alert';
  7. import Button from 'sentry/components/button';
  8. import Clipboard from 'sentry/components/clipboard';
  9. import DateTime from 'sentry/components/dateTime';
  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 {IconLink} from 'sentry/icons';
  21. import {t, tn} from 'sentry/locale';
  22. import space from 'sentry/styles/space';
  23. import {Organization} from 'sentry/types';
  24. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  25. import {generateEventSlug} from 'sentry/utils/discover/urls';
  26. import getDynamicText from 'sentry/utils/getDynamicText';
  27. import {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 {CustomerProfiler} from 'sentry/utils/performanceForSentry';
  31. import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
  32. import {Row, Tags, TransactionDetails, TransactionDetailsContainer} from './styles';
  33. type Props = {
  34. location: Location;
  35. organization: Organization;
  36. scrollToHash: (hash: string) => void;
  37. transaction: TraceFullDetailed;
  38. };
  39. class TransactionDetail extends Component<Props> {
  40. componentDidMount() {
  41. const {organization, transaction} = this.props;
  42. trackAdvancedAnalyticsEvent('performance_views.trace_view.open_transaction_details', {
  43. organization,
  44. operation: transaction['transaction.op'],
  45. transaction: transaction.transaction,
  46. });
  47. }
  48. renderTransactionErrors() {
  49. const {organization, transaction} = this.props;
  50. const {errors} = transaction;
  51. if (errors.length === 0) {
  52. return null;
  53. }
  54. return (
  55. <Alert
  56. system
  57. type="error"
  58. expand={errors.map(error => (
  59. <ErrorMessageContent key={error.event_id}>
  60. <ErrorDot level={error.level} />
  61. <ErrorLevel>{error.level}</ErrorLevel>
  62. <ErrorTitle>
  63. <Link to={generateIssueEventTarget(error, organization)}>
  64. {error.title}
  65. </Link>
  66. </ErrorTitle>
  67. </ErrorMessageContent>
  68. ))}
  69. >
  70. <ErrorMessageTitle>
  71. {tn(
  72. 'An error event occurred in this transaction.',
  73. '%s error events occurred in this transaction.',
  74. errors.length
  75. )}
  76. </ErrorMessageTitle>
  77. </Alert>
  78. );
  79. }
  80. renderGoToTransactionButton() {
  81. const {location, organization, transaction} = this.props;
  82. const eventSlug = generateEventSlug({
  83. id: transaction.event_id,
  84. project: transaction.project_slug,
  85. });
  86. const target = getTransactionDetailsUrl(
  87. organization.slug,
  88. eventSlug,
  89. transaction.transaction,
  90. omit(location.query, Object.values(PAGE_URL_PARAM))
  91. );
  92. return (
  93. <StyledButton size="xs" to={target}>
  94. {t('View Event')}
  95. </StyledButton>
  96. );
  97. }
  98. renderGoToSummaryButton() {
  99. const {location, organization, transaction} = this.props;
  100. const target = transactionSummaryRouteWithQuery({
  101. orgSlug: organization.slug,
  102. transaction: transaction.transaction,
  103. query: omit(location.query, Object.values(PAGE_URL_PARAM)),
  104. projectID: String(transaction.project_id),
  105. });
  106. return (
  107. <StyledButton size="xs" to={target}>
  108. {t('View Summary')}
  109. </StyledButton>
  110. );
  111. }
  112. renderMeasurements() {
  113. const {transaction} = this.props;
  114. const {measurements = {}} = transaction;
  115. const measurementKeys = Object.keys(measurements)
  116. .filter(name => Boolean(WEB_VITAL_DETAILS[`measurements.${name}`]))
  117. .sort();
  118. if (measurementKeys.length <= 0) {
  119. return null;
  120. }
  121. return (
  122. <Fragment>
  123. {measurementKeys.map(measurement => (
  124. <Row
  125. key={measurement}
  126. title={WEB_VITAL_DETAILS[`measurements.${measurement}`]?.name}
  127. >
  128. {`${Number(measurements[measurement].value.toFixed(3)).toLocaleString()}ms`}
  129. </Row>
  130. ))}
  131. </Fragment>
  132. );
  133. }
  134. scrollBarIntoView =
  135. (transactionId: string) => (e: React.MouseEvent<HTMLAnchorElement>) => {
  136. // do not use the default anchor behaviour
  137. // because it will be hidden behind the minimap
  138. e.preventDefault();
  139. const hash = `#txn-${transactionId}`;
  140. this.props.scrollToHash(hash);
  141. // TODO(txiao): This is causing a rerender of the whole page,
  142. // which can be slow.
  143. //
  144. // make sure to update the location
  145. browserHistory.push({
  146. ...this.props.location,
  147. hash,
  148. });
  149. };
  150. renderTransactionDetail() {
  151. const {location, organization, transaction} = this.props;
  152. const startTimestamp = Math.min(transaction.start_timestamp, transaction.timestamp);
  153. const endTimestamp = Math.max(transaction.start_timestamp, transaction.timestamp);
  154. const duration = (endTimestamp - startTimestamp) * 1000;
  155. const durationString = `${Number(duration.toFixed(3)).toLocaleString()}ms`;
  156. return (
  157. <TransactionDetails>
  158. <table className="table key-value">
  159. <tbody>
  160. <Row
  161. title={
  162. <TransactionIdTitle
  163. onClick={this.scrollBarIntoView(transaction.event_id)}
  164. >
  165. {t('Event ID')}
  166. <Clipboard
  167. value={`${window.location.href.replace(
  168. window.location.hash,
  169. ''
  170. )}#txn-${transaction.event_id}`}
  171. >
  172. <StyledIconLink />
  173. </Clipboard>
  174. </TransactionIdTitle>
  175. }
  176. extra={this.renderGoToTransactionButton()}
  177. >
  178. {transaction.event_id}
  179. </Row>
  180. <Row title="Transaction" extra={this.renderGoToSummaryButton()}>
  181. {transaction.transaction}
  182. </Row>
  183. <Row title="Transaction Status">{transaction['transaction.status']}</Row>
  184. <Row title="Span ID">{transaction.span_id}</Row>
  185. <Row title="Project">{transaction.project_slug}</Row>
  186. <Row title="Start Date">
  187. {getDynamicText({
  188. fixed: 'Mar 19, 2021 11:06:27 AM UTC',
  189. value: (
  190. <Fragment>
  191. <DateTime date={startTimestamp * 1000} />
  192. {` (${startTimestamp})`}
  193. </Fragment>
  194. ),
  195. })}
  196. </Row>
  197. <Row title="End Date">
  198. {getDynamicText({
  199. fixed: 'Mar 19, 2021 11:06:28 AM UTC',
  200. value: (
  201. <Fragment>
  202. <DateTime date={endTimestamp * 1000} />
  203. {` (${endTimestamp})`}
  204. </Fragment>
  205. ),
  206. })}
  207. </Row>
  208. <Row title="Duration">{durationString}</Row>
  209. <Row title="Operation">{transaction['transaction.op'] || ''}</Row>
  210. {this.renderMeasurements()}
  211. <Tags
  212. location={location}
  213. organization={organization}
  214. transaction={transaction}
  215. />
  216. </tbody>
  217. </table>
  218. </TransactionDetails>
  219. );
  220. }
  221. render() {
  222. return (
  223. <CustomerProfiler id="TransactionDetail">
  224. <TransactionDetailsContainer
  225. onClick={event => {
  226. // prevent toggling the transaction detail
  227. event.stopPropagation();
  228. }}
  229. >
  230. {this.renderTransactionErrors()}
  231. {this.renderTransactionDetail()}
  232. </TransactionDetailsContainer>
  233. </CustomerProfiler>
  234. );
  235. }
  236. }
  237. const TransactionIdTitle = styled('a')`
  238. display: flex;
  239. color: ${p => p.theme.textColor};
  240. :hover {
  241. color: ${p => p.theme.textColor};
  242. }
  243. `;
  244. const StyledIconLink = styled(IconLink)`
  245. display: block;
  246. color: ${p => p.theme.gray300};
  247. margin-left: ${space(1)};
  248. `;
  249. const StyledButton = styled(Button)`
  250. position: absolute;
  251. top: ${space(0.75)};
  252. right: ${space(0.5)};
  253. `;
  254. export default TransactionDetail;