eventMetas.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import {Button} from 'sentry/components/button';
  5. import Clipboard from 'sentry/components/clipboard';
  6. import DateTime from 'sentry/components/dateTime';
  7. import ContextIcon from 'sentry/components/events/contextSummary/contextIcon';
  8. import {generateIconName} from 'sentry/components/events/contextSummary/utils';
  9. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  10. import TimeSince from 'sentry/components/timeSince';
  11. import {Tooltip} from 'sentry/components/tooltip';
  12. import {frontend} from 'sentry/data/platformCategories';
  13. import {IconCopy, IconPlay} from 'sentry/icons';
  14. import {t} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import {AvatarProject, OrganizationSummary} from 'sentry/types';
  17. import {Event, EventTransaction} from 'sentry/types/event';
  18. import {getShortEventId} from 'sentry/utils/events';
  19. import {getDuration} from 'sentry/utils/formatters';
  20. import getDynamicText from 'sentry/utils/getDynamicText';
  21. import {
  22. QuickTraceQueryChildrenProps,
  23. TraceMeta,
  24. } from 'sentry/utils/performance/quickTrace/types';
  25. import {isTransaction} from 'sentry/utils/performance/quickTrace/utils';
  26. import Projects from 'sentry/utils/projects';
  27. import theme from 'sentry/utils/theme';
  28. import EventCreatedTooltip from 'sentry/views/issueDetails/eventCreatedTooltip';
  29. import QuickTraceMeta from './quickTraceMeta';
  30. import {MetaData} from './styles';
  31. type Props = Pick<
  32. React.ComponentProps<typeof QuickTraceMeta>,
  33. 'errorDest' | 'transactionDest'
  34. > & {
  35. event: Event;
  36. location: Location;
  37. meta: TraceMeta | null;
  38. organization: OrganizationSummary;
  39. projectId: string;
  40. quickTrace: QuickTraceQueryChildrenProps | null;
  41. };
  42. type State = {
  43. isLargeScreen: boolean;
  44. };
  45. /**
  46. * This should match the breakpoint chosen for the `EventDetailHeader` below
  47. */
  48. const BREAKPOINT_MEDIA_QUERY = `(min-width: ${theme.breakpoints.large})`;
  49. class EventMetas extends Component<Props, State> {
  50. state: State = {
  51. isLargeScreen: window.matchMedia?.(BREAKPOINT_MEDIA_QUERY)?.matches,
  52. };
  53. componentDidMount() {
  54. if (this.mq) {
  55. this.mq.addEventListener('change', this.handleMediaQueryChange);
  56. }
  57. }
  58. componentWillUnmount() {
  59. if (this.mq) {
  60. this.mq.removeEventListener('change', this.handleMediaQueryChange);
  61. }
  62. }
  63. mq = window.matchMedia?.(BREAKPOINT_MEDIA_QUERY);
  64. handleMediaQueryChange = (changed: MediaQueryListEvent) => {
  65. this.setState({
  66. isLargeScreen: changed.matches,
  67. });
  68. };
  69. render() {
  70. const {
  71. event,
  72. organization,
  73. projectId,
  74. location,
  75. quickTrace,
  76. meta,
  77. errorDest,
  78. transactionDest,
  79. } = this.props;
  80. const {isLargeScreen} = this.state;
  81. // Replay preview gets rendered as part of the breadcrumb section. We need
  82. // to check for presence of both to show the replay link button here.
  83. const hasReplay =
  84. organization.features.includes('session-replay-ui') &&
  85. Boolean(event.entries.find(({type}) => type === 'breadcrumbs')) &&
  86. Boolean(event?.tags?.find(({key}) => key === 'replayId')?.value);
  87. const type = isTransaction(event) ? 'transaction' : 'event';
  88. const timestamp = (
  89. <TimeSince
  90. tooltipBody={getDynamicText({
  91. value: (
  92. <EventCreatedTooltip
  93. event={{
  94. ...event,
  95. dateCreated:
  96. event.dateCreated ||
  97. new Date((event.endTimestamp || 0) * 1000).toISOString(),
  98. }}
  99. />
  100. ),
  101. fixed: 'Event Created Tooltip',
  102. })}
  103. date={event.dateCreated || (event.endTimestamp || 0) * 1000}
  104. />
  105. );
  106. const httpStatus = <HttpStatus event={event} />;
  107. return (
  108. <Projects orgId={organization.slug} slugs={[projectId]}>
  109. {({projects}) => {
  110. const project = projects.find(p => p.slug === projectId);
  111. return (
  112. <EventDetailHeader type={type} hasReplay={hasReplay}>
  113. <MetaData
  114. headingText={t('Event ID')}
  115. tooltipText={t('The unique ID assigned to this %s.', type)}
  116. bodyText={<EventID event={event} />}
  117. subtext={
  118. <ProjectBadge
  119. project={project ? project : {slug: projectId}}
  120. avatarSize={16}
  121. />
  122. }
  123. />
  124. {isTransaction(event) ? (
  125. <MetaData
  126. headingText={t('Event Duration')}
  127. tooltipText={t(
  128. 'The time elapsed between the start and end of this transaction.'
  129. )}
  130. bodyText={getDuration(
  131. event.endTimestamp - event.startTimestamp,
  132. 2,
  133. true
  134. )}
  135. subtext={timestamp}
  136. />
  137. ) : (
  138. <MetaData
  139. headingText={t('Created')}
  140. tooltipText={t('The time at which this event was created.')}
  141. bodyText={timestamp}
  142. subtext={getDynamicText({
  143. value: <DateTime date={event.dateCreated} />,
  144. fixed: 'May 6, 2021 3:27:01 UTC',
  145. })}
  146. />
  147. )}
  148. {isTransaction(event) && (
  149. <MetaData
  150. headingText={t('Status')}
  151. tooltipText={t(
  152. 'The status of this transaction indicating if it succeeded or otherwise.'
  153. )}
  154. bodyText={getStatusBodyText(project, event, meta)}
  155. subtext={httpStatus}
  156. />
  157. )}
  158. {isTransaction(event) &&
  159. (event.contexts.browser ? (
  160. <MetaData
  161. headingText={t('Browser')}
  162. tooltipText={t('The browser used in this transaction.')}
  163. bodyText={<BrowserDisplay event={event} />}
  164. subtext={event.contexts.browser?.version}
  165. />
  166. ) : (
  167. <span />
  168. ))}
  169. {hasReplay && (
  170. <ReplayButtonContainer>
  171. <Button href="#breadcrumbs" size="sm" icon={<IconPlay size="xs" />}>
  172. {t('Replay')}
  173. </Button>
  174. </ReplayButtonContainer>
  175. )}
  176. <QuickTraceContainer>
  177. <QuickTraceMeta
  178. event={event}
  179. project={project}
  180. location={location}
  181. quickTrace={quickTrace}
  182. traceMeta={meta}
  183. anchor={isLargeScreen ? 'right' : 'left'}
  184. errorDest={errorDest}
  185. transactionDest={transactionDest}
  186. />
  187. </QuickTraceContainer>
  188. </EventDetailHeader>
  189. );
  190. }}
  191. </Projects>
  192. );
  193. }
  194. }
  195. const BrowserCenter = styled('span')`
  196. display: flex;
  197. align-items: flex-start;
  198. gap: ${space(1)};
  199. `;
  200. const IconContainer = styled('div')`
  201. width: 20px;
  202. height: 20px;
  203. display: flex;
  204. flex-shrink: 0;
  205. align-items: center;
  206. justify-content: center;
  207. margin-top: ${space(0.25)};
  208. `;
  209. const BrowserDisplay = ({event}: {event: Event}) => {
  210. const icon = generateIconName(
  211. event.contexts.browser?.name,
  212. event.contexts.browser?.version
  213. );
  214. return (
  215. <BrowserCenter>
  216. <IconContainer>
  217. <ContextIcon name={icon} />
  218. </IconContainer>
  219. <span>{event.contexts.browser?.name}</span>
  220. </BrowserCenter>
  221. );
  222. };
  223. type EventDetailHeaderProps = {
  224. hasReplay: boolean;
  225. type?: 'transaction' | 'event';
  226. };
  227. function getEventDetailHeaderCols({hasReplay, type}: EventDetailHeaderProps): string {
  228. if (type === 'transaction') {
  229. return hasReplay
  230. ? 'grid-template-columns: minmax(160px, 1fr) minmax(160px, 1fr) minmax(160px, 1fr) minmax(160px, 1fr) 5fr minmax(325px, 1fr);'
  231. : 'grid-template-columns: minmax(160px, 1fr) minmax(160px, 1fr) minmax(160px, 1fr) minmax(160px, 1fr) 6fr;';
  232. }
  233. return hasReplay
  234. ? 'grid-template-columns: minmax(160px, 1fr) minmax(200px, 1fr) 5fr minmax(325px, 1fr);'
  235. : 'grid-template-columns: minmax(160px, 1fr) minmax(200px, 1fr) 6fr;';
  236. }
  237. const EventDetailHeader = styled('div')<EventDetailHeaderProps>`
  238. display: grid;
  239. grid-template-columns: repeat(${p => (p.type === 'transaction' ? 3 : 2)}, 1fr);
  240. grid-template-rows: repeat(2, auto);
  241. gap: ${space(2)};
  242. margin-bottom: ${space(2)};
  243. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  244. margin-bottom: 0;
  245. }
  246. /* This should match the breakpoint chosen for BREAKPOINT_MEDIA_QUERY above. */
  247. @media (min-width: ${p => p.theme.breakpoints.large}) {
  248. ${p => getEventDetailHeaderCols(p)};
  249. grid-row-gap: 0;
  250. }
  251. `;
  252. const ReplayButtonContainer = styled('div')`
  253. order: 2;
  254. display: flex;
  255. justify-content: flex-end;
  256. align-items: flex-start;
  257. @media (min-width: ${p => p.theme.breakpoints.large}) {
  258. order: 4;
  259. }
  260. `;
  261. const QuickTraceContainer = styled('div')`
  262. grid-column: 1 / -2;
  263. order: 1;
  264. @media (min-width: ${p => p.theme.breakpoints.large}) {
  265. order: 5;
  266. justify-self: flex-end;
  267. min-width: 325px;
  268. grid-column: unset;
  269. }
  270. `;
  271. function EventID({event}: {event: Event}) {
  272. return (
  273. <Clipboard value={event.eventID}>
  274. <EventIDContainer>
  275. <EventIDWrapper>{getShortEventId(event.eventID)}</EventIDWrapper>
  276. <Tooltip title={event.eventID} position="top">
  277. <IconCopy color="subText" />
  278. </Tooltip>
  279. </EventIDContainer>
  280. </Clipboard>
  281. );
  282. }
  283. const EventIDContainer = styled('div')`
  284. display: flex;
  285. align-items: center;
  286. cursor: pointer;
  287. `;
  288. const EventIDWrapper = styled('span')`
  289. margin-right: ${space(1)};
  290. `;
  291. export function HttpStatus({event}: {event: Event}) {
  292. const {tags} = event;
  293. const emptyStatus = <Fragment>{'\u2014'}</Fragment>;
  294. if (!Array.isArray(tags)) {
  295. return emptyStatus;
  296. }
  297. const tag = tags.find(tagObject => tagObject.key === 'http.status_code');
  298. if (!tag) {
  299. return emptyStatus;
  300. }
  301. return <Fragment>HTTP {tag.value}</Fragment>;
  302. }
  303. /*
  304. TODO: Ash
  305. 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
  306. 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.
  307. When that change is implemented, this function can simply be replaced with:
  308. event.contexts?.trace?.status ?? '\u2014';
  309. */
  310. export function getStatusBodyText(
  311. project: AvatarProject | undefined,
  312. event: EventTransaction,
  313. meta: TraceMeta | null
  314. ): string {
  315. const isFrontendProject = frontend.some(val => val === project?.platform);
  316. if (
  317. isFrontendProject &&
  318. meta &&
  319. meta.errors === 0 &&
  320. event.contexts?.trace?.status === 'unknown'
  321. ) {
  322. return 'ok';
  323. }
  324. return event.contexts?.trace?.status ?? '\u2014';
  325. }
  326. export default EventMetas;