eventMetas.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  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 DateTime from 'sentry/components/dateTime';
  6. import ContextIcon from 'sentry/components/events/contextSummary/contextIcon';
  7. import {generateIconName} from 'sentry/components/events/contextSummary/utils';
  8. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  9. import TimeSince from 'sentry/components/timeSince';
  10. import {Tooltip} from 'sentry/components/tooltip';
  11. import {backend} from 'sentry/data/platformCategories';
  12. import {IconCopy, IconPlay} from 'sentry/icons';
  13. import {t} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import {OrganizationSummary} from 'sentry/types';
  16. import {Event, EventTransaction} from 'sentry/types/event';
  17. import {getShortEventId} from 'sentry/utils/events';
  18. import {getDuration} from 'sentry/utils/formatters';
  19. import getDynamicText from 'sentry/utils/getDynamicText';
  20. import {
  21. QuickTraceQueryChildrenProps,
  22. TraceMeta,
  23. } from 'sentry/utils/performance/quickTrace/types';
  24. import {isTransaction} from 'sentry/utils/performance/quickTrace/utils';
  25. import Projects from 'sentry/utils/projects';
  26. import theme from 'sentry/utils/theme';
  27. import useCopyToClipboard from 'sentry/utils/useCopyToClipboard';
  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') &&
  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. return (
  107. <Projects orgId={organization.slug} slugs={[projectId]}>
  108. {({projects}) => {
  109. const project = projects.find(p => p.slug === projectId);
  110. const isBackendProject =
  111. !!project?.platform && backend.includes(project.platform as any);
  112. return (
  113. <EventDetailHeader
  114. type={type}
  115. isBackendProject={isBackendProject}
  116. hasReplay={hasReplay}
  117. >
  118. <MetaData
  119. headingText={t('Event ID')}
  120. tooltipText={t('The unique ID assigned to this %s.', type)}
  121. bodyText={<EventID event={event} />}
  122. subtext={
  123. <ProjectBadge
  124. project={project ? project : {slug: projectId}}
  125. avatarSize={16}
  126. />
  127. }
  128. />
  129. {isTransaction(event) ? (
  130. <MetaData
  131. headingText={t('Event Duration')}
  132. tooltipText={t(
  133. 'The time elapsed between the start and end of this transaction.'
  134. )}
  135. bodyText={getDuration(
  136. event.endTimestamp - event.startTimestamp,
  137. 2,
  138. true
  139. )}
  140. subtext={timestamp}
  141. />
  142. ) : (
  143. <MetaData
  144. headingText={t('Created')}
  145. tooltipText={t('The time at which this event was created.')}
  146. bodyText={timestamp}
  147. subtext={getDynamicText({
  148. value: <DateTime date={event.dateCreated} />,
  149. fixed: 'May 6, 2021 3:27:01 UTC',
  150. })}
  151. />
  152. )}
  153. {isTransaction(event) && isBackendProject && (
  154. <MetaData
  155. headingText={t('Status')}
  156. tooltipText={t(
  157. 'The status of this transaction indicating if it succeeded or otherwise.'
  158. )}
  159. bodyText={getStatusBodyText(event)}
  160. subtext={<HttpStatus event={event} />}
  161. />
  162. )}
  163. {isTransaction(event) &&
  164. (event.contexts.browser ? (
  165. <MetaData
  166. headingText={t('Browser')}
  167. tooltipText={t('The browser used in this transaction.')}
  168. bodyText={<BrowserDisplay event={event} />}
  169. subtext={event.contexts.browser?.version}
  170. />
  171. ) : (
  172. <span />
  173. ))}
  174. {hasReplay && (
  175. <ReplayButtonContainer>
  176. <Button href="#replay" size="sm" icon={<IconPlay size="xs" />}>
  177. {t('Replay')}
  178. </Button>
  179. </ReplayButtonContainer>
  180. )}
  181. <QuickTraceContainer>
  182. <QuickTraceMeta
  183. event={event}
  184. project={project}
  185. location={location}
  186. quickTrace={quickTrace}
  187. traceMeta={meta}
  188. anchor={isLargeScreen ? 'right' : 'left'}
  189. errorDest={errorDest}
  190. transactionDest={transactionDest}
  191. />
  192. </QuickTraceContainer>
  193. </EventDetailHeader>
  194. );
  195. }}
  196. </Projects>
  197. );
  198. }
  199. }
  200. const BrowserCenter = styled('span')`
  201. display: flex;
  202. align-items: flex-start;
  203. gap: ${space(1)};
  204. `;
  205. const IconContainer = styled('div')`
  206. width: 20px;
  207. height: 20px;
  208. display: flex;
  209. flex-shrink: 0;
  210. align-items: center;
  211. justify-content: center;
  212. margin-top: ${space(0.25)};
  213. `;
  214. function BrowserDisplay({event}: {event: Event}) {
  215. const icon = generateIconName(
  216. event.contexts.browser?.name,
  217. event.contexts.browser?.version
  218. );
  219. return (
  220. <BrowserCenter>
  221. <IconContainer>
  222. <ContextIcon name={icon} />
  223. </IconContainer>
  224. <span>{event.contexts.browser?.name}</span>
  225. </BrowserCenter>
  226. );
  227. }
  228. type EventDetailHeaderProps = {
  229. hasReplay: boolean;
  230. isBackendProject: boolean;
  231. type?: 'transaction' | 'event';
  232. };
  233. export function getEventDetailHeaderCols({
  234. hasReplay,
  235. isBackendProject,
  236. type,
  237. }: EventDetailHeaderProps): string {
  238. return `grid-template-columns: ${[
  239. 'minmax(160px, 1fr)', // Event ID
  240. type === 'transaction' ? 'minmax(160px, 1fr)' : 'minmax(200px, 1fr)', // Duration or Created Time
  241. type === 'transaction' && isBackendProject && 'minmax(160px, 1fr)', // Status
  242. type === 'transaction' && 'minmax(160px, 1fr) ', // Browser
  243. hasReplay ? '5fr' : '6fr', // Replay
  244. hasReplay && 'minmax(325px, 1fr)', // Quick Trace
  245. ]
  246. .filter(Boolean)
  247. .join(' ')};`;
  248. }
  249. const EventDetailHeader = styled('div')<EventDetailHeaderProps>`
  250. display: grid;
  251. grid-template-columns: repeat(${p => (p.type === 'transaction' ? 3 : 2)}, 1fr);
  252. grid-template-rows: repeat(2, auto);
  253. gap: ${space(2)};
  254. margin-bottom: ${space(2)};
  255. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  256. margin-bottom: 0;
  257. }
  258. /* This should match the breakpoint chosen for BREAKPOINT_MEDIA_QUERY above. */
  259. @media (min-width: ${p => p.theme.breakpoints.large}) {
  260. ${p => getEventDetailHeaderCols(p)};
  261. grid-row-gap: 0;
  262. }
  263. `;
  264. const ReplayButtonContainer = styled('div')`
  265. order: 2;
  266. display: flex;
  267. justify-content: flex-end;
  268. align-items: flex-start;
  269. @media (min-width: ${p => p.theme.breakpoints.large}) {
  270. order: 4;
  271. }
  272. `;
  273. const QuickTraceContainer = styled('div')`
  274. grid-column: 1 / -2;
  275. order: 1;
  276. @media (min-width: ${p => p.theme.breakpoints.large}) {
  277. order: 5;
  278. justify-self: flex-end;
  279. min-width: 325px;
  280. grid-column: unset;
  281. }
  282. `;
  283. function EventID({event}: {event: Event}) {
  284. const {onClick} = useCopyToClipboard({text: event.eventID});
  285. return (
  286. <EventIDContainer onClick={onClick}>
  287. <Tooltip title={event.eventID} position="top">
  288. <EventIDWrapper>{getShortEventId(event.eventID)}</EventIDWrapper>
  289. <IconCopy />
  290. </Tooltip>
  291. </EventIDContainer>
  292. );
  293. }
  294. const EventIDContainer = styled('button')`
  295. display: flex;
  296. align-items: center;
  297. background: transparent;
  298. border: none;
  299. padding: 0;
  300. &:hover {
  301. color: ${p => p.theme.activeText};
  302. }
  303. svg {
  304. color: ${p => p.theme.subText};
  305. }
  306. &:hover svg {
  307. color: ${p => p.theme.textColor};
  308. }
  309. `;
  310. const EventIDWrapper = styled('span')`
  311. margin-right: ${space(1)};
  312. `;
  313. export function HttpStatus({event}: {event: Event}) {
  314. const {tags} = event;
  315. const emptyStatus = <Fragment>{'\u2014'}</Fragment>;
  316. if (!Array.isArray(tags)) {
  317. return emptyStatus;
  318. }
  319. const tag = tags.find(tagObject => tagObject.key === 'http.status_code');
  320. if (!tag) {
  321. return emptyStatus;
  322. }
  323. return <Fragment>HTTP {tag.value}</Fragment>;
  324. }
  325. export function getStatusBodyText(event: EventTransaction): string {
  326. return event.contexts?.trace?.status ?? '\u2014';
  327. }
  328. export default EventMetas;