eventMetas.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {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 type {OrganizationSummary} from 'sentry/types';
  16. import type {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 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 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 />}>
  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. export function BrowserDisplay({
  215. event,
  216. showVersion = false,
  217. }: {
  218. event: Event;
  219. showVersion?: boolean;
  220. }) {
  221. const icon = generateIconName(
  222. event.contexts.browser?.name,
  223. event.contexts.browser?.version
  224. );
  225. return (
  226. <BrowserCenter>
  227. <IconContainer>
  228. <ContextIcon name={icon} />
  229. </IconContainer>
  230. <span>
  231. {event.contexts.browser?.name} {showVersion && event.contexts.browser?.version}
  232. </span>
  233. </BrowserCenter>
  234. );
  235. }
  236. type EventDetailHeaderProps = {
  237. hasReplay: boolean;
  238. isBackendProject: boolean;
  239. type?: 'transaction' | 'event';
  240. };
  241. export function getEventDetailHeaderCols({
  242. hasReplay,
  243. isBackendProject,
  244. type,
  245. }: EventDetailHeaderProps): string {
  246. return `grid-template-columns: ${[
  247. 'minmax(160px, 1fr)', // Event ID
  248. type === 'transaction' ? 'minmax(160px, 1fr)' : 'minmax(200px, 1fr)', // Duration or Created Time
  249. type === 'transaction' && isBackendProject && 'minmax(160px, 1fr)', // Status
  250. type === 'transaction' && 'minmax(160px, 1fr) ', // Browser
  251. hasReplay ? '5fr' : '6fr', // Replay
  252. hasReplay && 'minmax(325px, 1fr)', // Quick Trace
  253. ]
  254. .filter(Boolean)
  255. .join(' ')};`;
  256. }
  257. const EventDetailHeader = styled('div')<EventDetailHeaderProps>`
  258. display: grid;
  259. grid-template-columns: repeat(${p => (p.type === 'transaction' ? 3 : 2)}, 1fr);
  260. grid-template-rows: repeat(2, auto);
  261. gap: ${space(2)};
  262. margin-bottom: ${space(2)};
  263. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  264. margin-bottom: 0;
  265. }
  266. /* This should match the breakpoint chosen for BREAKPOINT_MEDIA_QUERY above. */
  267. @media (min-width: ${p => p.theme.breakpoints.large}) {
  268. ${p => getEventDetailHeaderCols(p)};
  269. grid-row-gap: 0;
  270. }
  271. `;
  272. const ReplayButtonContainer = styled('div')`
  273. order: 2;
  274. display: flex;
  275. justify-content: flex-end;
  276. align-items: flex-start;
  277. @media (min-width: ${p => p.theme.breakpoints.large}) {
  278. order: 4;
  279. }
  280. `;
  281. const QuickTraceContainer = styled('div')`
  282. grid-column: 1 / -2;
  283. order: 1;
  284. @media (min-width: ${p => p.theme.breakpoints.large}) {
  285. order: 5;
  286. justify-self: flex-end;
  287. min-width: 325px;
  288. grid-column: unset;
  289. }
  290. `;
  291. function EventID({event}: {event: Event}) {
  292. const {onClick} = useCopyToClipboard({text: event.eventID});
  293. return (
  294. <EventIDContainer onClick={onClick}>
  295. <Tooltip title={event.eventID} position="top">
  296. <EventIDWrapper>{getShortEventId(event.eventID)}</EventIDWrapper>
  297. <IconCopy />
  298. </Tooltip>
  299. </EventIDContainer>
  300. );
  301. }
  302. const EventIDContainer = styled('button')`
  303. display: flex;
  304. align-items: center;
  305. background: transparent;
  306. border: none;
  307. padding: 0;
  308. &:hover {
  309. color: ${p => p.theme.activeText};
  310. }
  311. svg {
  312. color: ${p => p.theme.subText};
  313. }
  314. &:hover svg {
  315. color: ${p => p.theme.textColor};
  316. }
  317. `;
  318. const EventIDWrapper = styled('span')`
  319. margin-right: ${space(1)};
  320. `;
  321. export function HttpStatus({event}: {event: Event}) {
  322. const {tags} = event;
  323. const emptyStatus = <Fragment>{'\u2014'}</Fragment>;
  324. if (!Array.isArray(tags)) {
  325. return emptyStatus;
  326. }
  327. const tag = tags.find(tagObject => tagObject.key === 'http.status_code');
  328. if (!tag) {
  329. return emptyStatus;
  330. }
  331. return <Fragment>HTTP {tag.value}</Fragment>;
  332. }
  333. export function getStatusBodyText(event: EventTransaction): string {
  334. return event.contexts?.trace?.status ?? '\u2014';
  335. }
  336. export default EventMetas;