content.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import {Fragment, useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import Alert from 'sentry/components/alert';
  4. import {Button, LinkButton} from 'sentry/components/button';
  5. import ButtonBar from 'sentry/components/buttonBar';
  6. import NotFound from 'sentry/components/errors/notFound';
  7. import EventCustomPerformanceMetrics, {
  8. EventDetailPageSource,
  9. } from 'sentry/components/events/eventCustomPerformanceMetrics';
  10. import {BorderlessEventEntries} from 'sentry/components/events/eventEntries';
  11. import EventMetadata from 'sentry/components/events/eventMetadata';
  12. import EventVitals from 'sentry/components/events/eventVitals';
  13. import getUrlFromEvent from 'sentry/components/events/interfaces/request/getUrlFromEvent';
  14. import * as SpanEntryContext from 'sentry/components/events/interfaces/spans/context';
  15. import RootSpanStatus from 'sentry/components/events/rootSpanStatus';
  16. import FileSize from 'sentry/components/fileSize';
  17. import * as Layout from 'sentry/components/layouts/thirds';
  18. import LoadingError from 'sentry/components/loadingError';
  19. import LoadingIndicator from 'sentry/components/loadingIndicator';
  20. import {TransactionToProfileButton} from 'sentry/components/profiling/transactionToProfileButton';
  21. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  22. import {TagsTable} from 'sentry/components/tagsTable';
  23. import {Tooltip} from 'sentry/components/tooltip';
  24. import {IconOpen} from 'sentry/icons';
  25. import {t} from 'sentry/locale';
  26. import type {Event, EventTag, EventTransaction} from 'sentry/types/event';
  27. import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
  28. import type {Organization} from 'sentry/types/organization';
  29. import type {Project} from 'sentry/types/project';
  30. import {formatTagKey} from 'sentry/utils/discover/fields';
  31. import {getAnalyticsDataForEvent} from 'sentry/utils/events';
  32. import {QuickTraceContext} from 'sentry/utils/performance/quickTrace/quickTraceContext';
  33. import QuickTraceQuery from 'sentry/utils/performance/quickTrace/quickTraceQuery';
  34. import TraceMetaQuery from 'sentry/utils/performance/quickTrace/traceMetaQuery';
  35. import {
  36. getTraceTimeRangeFromEvent,
  37. isTransaction,
  38. } from 'sentry/utils/performance/quickTrace/utils';
  39. import {getTransactionDetailsUrl} from 'sentry/utils/performance/urls';
  40. import Projects from 'sentry/utils/projects';
  41. import {useApiQuery} from 'sentry/utils/queryClient';
  42. import {appendTagCondition, decodeScalar} from 'sentry/utils/queryString';
  43. import type {WithRouteAnalyticsProps} from 'sentry/utils/routeAnalytics/withRouteAnalytics';
  44. import withRouteAnalytics from 'sentry/utils/routeAnalytics/withRouteAnalytics';
  45. import Breadcrumb from 'sentry/views/performance/breadcrumb';
  46. import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider';
  47. import {ProfileContext, ProfilesProvider} from 'sentry/views/profiling/profilesProvider';
  48. import TraceDetailsRouting from '../traceDetails/TraceDetailsRouting';
  49. import {transactionSummaryRouteWithQuery} from '../transactionSummary/utils';
  50. import {getSelectedProjectPlatforms} from '../utils';
  51. import EventMetas from './eventMetas';
  52. import FinishSetupAlert from './finishSetupAlert';
  53. type Props = Pick<RouteComponentProps<{eventSlug: string}, {}>, 'params' | 'location'> &
  54. WithRouteAnalyticsProps & {
  55. eventSlug: string;
  56. organization: Organization;
  57. projects: Project[];
  58. };
  59. function EventDetailsContent(props: Props) {
  60. const [isSidebarVisible, setIsSidebarVisible] = useState<boolean>(true);
  61. const projectId = props.eventSlug.split(':')[0]!;
  62. const {organization, eventSlug, location} = props;
  63. const {
  64. data: event,
  65. isPending,
  66. error,
  67. } = useApiQuery<Event>(
  68. [`/organizations/${organization.slug}/events/${eventSlug}/`],
  69. {staleTime: 2 * 60 * 1000} // 2 minutes in milliseonds
  70. );
  71. useEffect(() => {
  72. if (event) {
  73. const {projects} = props;
  74. props.setEventNames(
  75. 'performance.event_details',
  76. 'Performance: Opened Event Details'
  77. );
  78. props.setRouteAnalyticsParams({
  79. event_type: event?.type,
  80. project_platforms: getSelectedProjectPlatforms(location, projects),
  81. ...getAnalyticsDataForEvent(event),
  82. });
  83. }
  84. }, [event, props, location]);
  85. const generateTagUrl = (tag: EventTag) => {
  86. if (!event) {
  87. return '';
  88. }
  89. const query = decodeScalar(location.query.query, '');
  90. const newQuery = {
  91. ...location.query,
  92. query: appendTagCondition(query, formatTagKey(tag.key), tag.value),
  93. };
  94. return transactionSummaryRouteWithQuery({
  95. organization,
  96. transaction: event.title,
  97. projectID: event.projectID,
  98. query: newQuery,
  99. });
  100. };
  101. function renderContent(transaction: Event) {
  102. const transactionName = transaction.title;
  103. const query = decodeScalar(location.query.query, '');
  104. const eventJsonUrl = `/api/0/projects/${organization.slug}/${projectId}/events/${transaction.eventID}/json/`;
  105. const traceId = transaction.contexts?.trace?.trace_id ?? '';
  106. const {start, end} = getTraceTimeRangeFromEvent(transaction);
  107. const hasProfilingFeature = organization.features.includes('profiling');
  108. const profileId =
  109. (transaction as EventTransaction).contexts?.profile?.profile_id ?? null;
  110. const originatingUrl = getUrlFromEvent(transaction);
  111. return (
  112. <TraceMetaQuery
  113. location={location}
  114. orgSlug={organization.slug}
  115. traceId={traceId}
  116. start={start}
  117. end={end}
  118. >
  119. {metaResults => (
  120. <QuickTraceQuery
  121. event={transaction}
  122. location={location}
  123. orgSlug={organization.slug}
  124. skipLight={false}
  125. >
  126. {results => (
  127. <Fragment>
  128. <Layout.Header>
  129. <Layout.HeaderContent>
  130. <Breadcrumb
  131. organization={organization}
  132. location={location}
  133. transaction={{
  134. project: transaction.projectID,
  135. name: transactionName,
  136. }}
  137. eventSlug={eventSlug}
  138. />
  139. <Layout.Title data-test-id="event-header">
  140. <Tooltip showOnlyOnOverflow skipWrapper title={transactionName}>
  141. <EventTitle>{transaction.title}</EventTitle>
  142. </Tooltip>
  143. {originatingUrl && (
  144. <LinkButton
  145. aria-label={t('Go to originating URL')}
  146. size="zero"
  147. icon={<IconOpen />}
  148. href={originatingUrl}
  149. external
  150. translucentBorder
  151. borderless
  152. />
  153. )}
  154. </Layout.Title>
  155. </Layout.HeaderContent>
  156. <Layout.HeaderActions>
  157. <ButtonBar gap={1}>
  158. <Button
  159. size="sm"
  160. onClick={() => setIsSidebarVisible(prev => !prev)}
  161. >
  162. {isSidebarVisible ? 'Hide Details' : 'Show Details'}
  163. </Button>
  164. {results && (
  165. <LinkButton
  166. size="sm"
  167. icon={<IconOpen />}
  168. href={eventJsonUrl}
  169. external
  170. >
  171. {t('JSON')} (<FileSize bytes={transaction.size} />)
  172. </LinkButton>
  173. )}
  174. {hasProfilingFeature && isTransaction(transaction) && (
  175. <TransactionToProfileButton
  176. event={transaction}
  177. projectSlug={projectId}
  178. />
  179. )}
  180. </ButtonBar>
  181. </Layout.HeaderActions>
  182. </Layout.Header>
  183. <Layout.Body>
  184. {results && (
  185. <Layout.Main fullWidth>
  186. <EventMetas
  187. quickTrace={results}
  188. meta={metaResults?.meta ?? null}
  189. event={transaction}
  190. organization={organization}
  191. projectId={projectId}
  192. location={location}
  193. errorDest="issue"
  194. transactionDest="performance"
  195. />
  196. </Layout.Main>
  197. )}
  198. <Layout.Main fullWidth={!isSidebarVisible}>
  199. <Projects orgId={organization.slug} slugs={[projectId]}>
  200. {({projects: _projects}) => (
  201. <SpanEntryContext.Provider
  202. value={{
  203. getViewChildTransactionTarget: childTransactionProps => {
  204. return getTransactionDetailsUrl(
  205. organization.slug,
  206. childTransactionProps.eventSlug,
  207. childTransactionProps.transaction,
  208. location.query
  209. );
  210. },
  211. }}
  212. >
  213. <QuickTraceContext.Provider value={results}>
  214. {hasProfilingFeature ? (
  215. <ProfilesProvider
  216. orgSlug={organization.slug}
  217. projectSlug={projectId}
  218. profileMeta={profileId || ''}
  219. >
  220. <ProfileContext.Consumer>
  221. {profiles => (
  222. <ProfileGroupProvider
  223. type="flamechart"
  224. input={
  225. profiles?.type === 'resolved'
  226. ? profiles.data
  227. : null
  228. }
  229. traceID={profileId || ''}
  230. >
  231. <BorderlessEventEntries
  232. organization={organization}
  233. event={event}
  234. project={_projects[0] as Project}
  235. showTagSummary={false}
  236. />
  237. </ProfileGroupProvider>
  238. )}
  239. </ProfileContext.Consumer>
  240. </ProfilesProvider>
  241. ) : (
  242. <BorderlessEventEntries
  243. organization={organization}
  244. event={event}
  245. project={_projects[0] as Project}
  246. showTagSummary={false}
  247. />
  248. )}
  249. </QuickTraceContext.Provider>
  250. </SpanEntryContext.Provider>
  251. )}
  252. </Projects>
  253. </Layout.Main>
  254. {isSidebarVisible && (
  255. <Layout.Side>
  256. {results === undefined && (
  257. <Fragment>
  258. <EventMetadata
  259. event={transaction}
  260. organization={organization}
  261. projectId={projectId}
  262. />
  263. <RootSpanStatus event={transaction} />
  264. </Fragment>
  265. )}
  266. <EventVitals event={transaction} />
  267. <EventCustomPerformanceMetrics
  268. event={transaction}
  269. location={location}
  270. organization={organization}
  271. source={EventDetailPageSource.PERFORMANCE}
  272. />
  273. <TagsTable
  274. event={transaction}
  275. query={query}
  276. generateUrl={generateTagUrl}
  277. />
  278. </Layout.Side>
  279. )}
  280. </Layout.Body>
  281. </Fragment>
  282. )}
  283. </QuickTraceQuery>
  284. )}
  285. </TraceMetaQuery>
  286. );
  287. }
  288. function renderBody() {
  289. if (!event) {
  290. return <NotFound />;
  291. }
  292. const isSampleTransaction = event.tags.some(
  293. tag => tag.key === 'sample_event' && tag.value === 'yes'
  294. );
  295. return (
  296. <TraceDetailsRouting event={event}>
  297. <Fragment>
  298. {isSampleTransaction && (
  299. <FinishSetupAlert organization={organization} projectId={projectId} />
  300. )}
  301. {renderContent(event)}
  302. </Fragment>
  303. </TraceDetailsRouting>
  304. );
  305. }
  306. if (isPending) {
  307. return <LoadingIndicator />;
  308. }
  309. if (error) {
  310. const notFound = error.status === 404;
  311. const permissionDenied = error.status === 403;
  312. if (notFound) {
  313. return <NotFound />;
  314. }
  315. if (permissionDenied) {
  316. return (
  317. <LoadingError message={t('You do not have permission to view that event.')} />
  318. );
  319. }
  320. return (
  321. <Alert type="error" showIcon>
  322. {error.message}
  323. </Alert>
  324. );
  325. }
  326. return (
  327. <SentryDocumentTitle
  328. title={t('Performance — Event Details')}
  329. orgSlug={organization.slug}
  330. >
  331. {renderBody() as React.ReactChild}
  332. </SentryDocumentTitle>
  333. );
  334. }
  335. // We can't use theme.overflowEllipsis so that width isn't set to 100%
  336. // since button withn a link has to immediately follow the text in the title
  337. const EventTitle = styled('div')`
  338. display: block;
  339. white-space: nowrap;
  340. overflow: hidden;
  341. text-overflow: ellipsis;
  342. `;
  343. export default withRouteAnalytics(EventDetailsContent);