content.tsx 14 KB

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