eventMetas.tsx 11 KB

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