eventMetas.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import Clipboard from 'sentry/components/clipboard';
  5. import DateTime from 'sentry/components/dateTime';
  6. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  7. import TimeSince from 'sentry/components/timeSince';
  8. import Tooltip from 'sentry/components/tooltip';
  9. import {frontend} from 'sentry/data/platformCategories';
  10. import {IconCopy} from 'sentry/icons';
  11. import {t} from 'sentry/locale';
  12. import space from 'sentry/styles/space';
  13. import {AvatarProject, OrganizationSummary} from 'sentry/types';
  14. import {Event, EventTransaction} from 'sentry/types/event';
  15. import {getShortEventId} from 'sentry/utils/events';
  16. import {getDuration} from 'sentry/utils/formatters';
  17. import getDynamicText from 'sentry/utils/getDynamicText';
  18. import {
  19. QuickTraceQueryChildrenProps,
  20. TraceMeta,
  21. } from 'sentry/utils/performance/quickTrace/types';
  22. import {isTransaction} from 'sentry/utils/performance/quickTrace/utils';
  23. import Projects from 'sentry/utils/projects';
  24. import theme from 'sentry/utils/theme';
  25. import QuickTraceMeta from './quickTraceMeta';
  26. import {MetaData} from './styles';
  27. type Props = Pick<
  28. React.ComponentProps<typeof QuickTraceMeta>,
  29. 'errorDest' | 'transactionDest'
  30. > & {
  31. event: Event;
  32. location: Location;
  33. meta: TraceMeta | null;
  34. organization: OrganizationSummary;
  35. projectId: string;
  36. quickTrace: QuickTraceQueryChildrenProps | null;
  37. };
  38. type State = {
  39. isLargeScreen: boolean;
  40. };
  41. /**
  42. * This should match the breakpoint chosen for the `EventDetailHeader` below
  43. */
  44. const BREAKPOINT_MEDIA_QUERY = `(min-width: ${theme.breakpoints.large})`;
  45. class EventMetas extends Component<Props, State> {
  46. state: State = {
  47. isLargeScreen: window.matchMedia?.(BREAKPOINT_MEDIA_QUERY)?.matches,
  48. };
  49. componentDidMount() {
  50. if (this.mq) {
  51. this.mq.addEventListener('change', this.handleMediaQueryChange);
  52. }
  53. }
  54. componentWillUnmount() {
  55. if (this.mq) {
  56. this.mq.removeEventListener('change', this.handleMediaQueryChange);
  57. }
  58. }
  59. mq = window.matchMedia?.(BREAKPOINT_MEDIA_QUERY);
  60. handleMediaQueryChange = (changed: MediaQueryListEvent) => {
  61. this.setState({
  62. isLargeScreen: changed.matches,
  63. });
  64. };
  65. render() {
  66. const {
  67. event,
  68. organization,
  69. projectId,
  70. location,
  71. quickTrace,
  72. meta,
  73. errorDest,
  74. transactionDest,
  75. } = this.props;
  76. const {isLargeScreen} = this.state;
  77. const type = isTransaction(event) ? 'transaction' : 'event';
  78. const timestamp = (
  79. <TimeSince date={event.dateCreated || (event.endTimestamp || 0) * 1000} />
  80. );
  81. const httpStatus = <HttpStatus event={event} />;
  82. return (
  83. <Projects orgId={organization.slug} slugs={[projectId]}>
  84. {({projects}) => {
  85. const project = projects.find(p => p.slug === projectId);
  86. return (
  87. <EventDetailHeader type={type}>
  88. <MetaData
  89. headingText={t('Event ID')}
  90. tooltipText={t('The unique ID assigned to this %s.', type)}
  91. bodyText={<EventID event={event} />}
  92. subtext={
  93. <ProjectBadge
  94. project={project ? project : {slug: projectId}}
  95. avatarSize={16}
  96. />
  97. }
  98. />
  99. {isTransaction(event) ? (
  100. <MetaData
  101. headingText={t('Event Duration')}
  102. tooltipText={t(
  103. 'The time elapsed between the start and end of this transaction.'
  104. )}
  105. bodyText={getDuration(
  106. event.endTimestamp - event.startTimestamp,
  107. 2,
  108. true
  109. )}
  110. subtext={timestamp}
  111. />
  112. ) : (
  113. <MetaData
  114. headingText={t('Created')}
  115. tooltipText={t('The time at which this event was created.')}
  116. bodyText={timestamp}
  117. subtext={getDynamicText({
  118. value: <DateTime date={event.dateCreated} />,
  119. fixed: 'May 6, 2021 3:27:01 UTC',
  120. })}
  121. />
  122. )}
  123. {isTransaction(event) && (
  124. <MetaData
  125. headingText={t('Status')}
  126. tooltipText={t(
  127. 'The status of this transaction indicating if it succeeded or otherwise.'
  128. )}
  129. bodyText={getStatusBodyText(project, event, meta)}
  130. subtext={httpStatus}
  131. />
  132. )}
  133. <QuickTraceContainer>
  134. <QuickTraceMeta
  135. event={event}
  136. project={project}
  137. location={location}
  138. quickTrace={quickTrace}
  139. traceMeta={meta}
  140. anchor={isLargeScreen ? 'right' : 'left'}
  141. errorDest={errorDest}
  142. transactionDest={transactionDest}
  143. />
  144. </QuickTraceContainer>
  145. </EventDetailHeader>
  146. );
  147. }}
  148. </Projects>
  149. );
  150. }
  151. }
  152. const EventDetailHeader = styled('div')<{type?: 'transaction' | 'event'}>`
  153. display: grid;
  154. grid-template-columns: repeat(${p => (p.type === 'transaction' ? 3 : 2)}, 1fr);
  155. grid-template-rows: repeat(2, auto);
  156. gap: ${space(2)};
  157. margin-bottom: ${space(2)};
  158. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  159. margin-bottom: 0;
  160. }
  161. /* This should match the breakpoint chosen for BREAKPOINT_MEDIA_QUERY above. */
  162. @media (min-width: ${p => p.theme.breakpoints.large}) {
  163. ${p =>
  164. p.type === 'transaction'
  165. ? 'grid-template-columns: minmax(160px, 1fr) minmax(160px, 1fr) minmax(160px, 1fr) 6fr;'
  166. : 'grid-template-columns: minmax(160px, 1fr) minmax(200px, 1fr) 6fr;'};
  167. grid-row-gap: 0;
  168. }
  169. `;
  170. const QuickTraceContainer = styled('div')`
  171. grid-column: 1/4;
  172. @media (min-width: ${p => p.theme.breakpoints.large}) {
  173. justify-self: flex-end;
  174. min-width: 325px;
  175. grid-column: unset;
  176. }
  177. `;
  178. function EventID({event}: {event: Event}) {
  179. return (
  180. <Clipboard value={event.eventID}>
  181. <EventIDContainer>
  182. <EventIDWrapper>{getShortEventId(event.eventID)}</EventIDWrapper>
  183. <Tooltip title={event.eventID} position="top">
  184. <IconCopy color="subText" />
  185. </Tooltip>
  186. </EventIDContainer>
  187. </Clipboard>
  188. );
  189. }
  190. const EventIDContainer = styled('div')`
  191. display: flex;
  192. align-items: center;
  193. cursor: pointer;
  194. `;
  195. const EventIDWrapper = styled('span')`
  196. margin-right: ${space(1)};
  197. `;
  198. function HttpStatus({event}: {event: Event}) {
  199. const {tags} = event;
  200. const emptyStatus = <Fragment>{'\u2014'}</Fragment>;
  201. if (!Array.isArray(tags)) {
  202. return emptyStatus;
  203. }
  204. const tag = tags.find(tagObject => tagObject.key === 'http.status_code');
  205. if (!tag) {
  206. return emptyStatus;
  207. }
  208. return <Fragment>HTTP {tag.value}</Fragment>;
  209. }
  210. /*
  211. TODO: Ash
  212. I put this in place as a temporary patch to prevent successful frontend transactions from being set as 'unknown', which is what Relay sets by default
  213. if there is no status set by the SDK. In the future, the possible statuses will be revised and frontend transactions should properly have a status set.
  214. When that change is implemented, this function can simply be replaced with:
  215. event.contexts?.trace?.status ?? '\u2014';
  216. */
  217. function getStatusBodyText(
  218. project: AvatarProject | undefined,
  219. event: EventTransaction,
  220. meta: TraceMeta | null
  221. ): string {
  222. const isFrontendProject = frontend.some(val => val === project?.platform);
  223. if (
  224. isFrontendProject &&
  225. meta &&
  226. meta.errors === 0 &&
  227. event.contexts?.trace?.status === 'unknown'
  228. ) {
  229. return 'ok';
  230. }
  231. return event.contexts?.trace?.status ?? '\u2014';
  232. }
  233. export default EventMetas;