newTraceDetailsContent.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import {Fragment} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Alert} from 'sentry/components/alert';
  5. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  6. import ButtonBar from 'sentry/components/buttonBar';
  7. import DiscoverButton from 'sentry/components/discoverButton';
  8. import * as Layout from 'sentry/components/layouts/thirds';
  9. import ExternalLink from 'sentry/components/links/externalLink';
  10. import LoadingError from 'sentry/components/loadingError';
  11. import LoadingIndicator from 'sentry/components/loadingIndicator';
  12. import TimeSince from 'sentry/components/timeSince';
  13. import {t, tct, tn} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import {Organization} from 'sentry/types';
  16. import {trackAnalytics} from 'sentry/utils/analytics';
  17. import EventView from 'sentry/utils/discover/eventView';
  18. import {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
  19. import {getDuration} from 'sentry/utils/formatters';
  20. import getDynamicText from 'sentry/utils/getDynamicText';
  21. import {
  22. TraceError,
  23. TraceFullDetailed,
  24. TraceMeta,
  25. } from 'sentry/utils/performance/quickTrace/types';
  26. import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
  27. import Breadcrumb from 'sentry/views/performance/breadcrumb';
  28. import {MetaData} from 'sentry/views/performance/transactionDetails/styles';
  29. import {TraceDetailHeader} from './styles';
  30. import TraceNotFound from './traceNotFound';
  31. import TraceView from './traceView';
  32. import {TraceInfo} from './types';
  33. import {getTraceInfo, hasTraceData, isRootTransaction} from './utils';
  34. type Props = Pick<RouteComponentProps<{traceSlug: string}, {}>, 'params' | 'location'> & {
  35. dateSelected: boolean;
  36. error: QueryError | null;
  37. isLoading: boolean;
  38. meta: TraceMeta | null;
  39. organization: Organization;
  40. traceEventView: EventView;
  41. traceSlug: string;
  42. traces: TraceFullDetailed[] | null;
  43. handleLimitChange?: (newLimit: number) => void;
  44. orphanErrors?: TraceError[];
  45. };
  46. function NewTraceDetailsContent(props: Props) {
  47. const renderTraceLoading = () => {
  48. return (
  49. <LoadingContainer>
  50. <StyledLoadingIndicator />
  51. {t('Hang in there, as we build your trace view!')}
  52. </LoadingContainer>
  53. );
  54. };
  55. const renderTraceRequiresDateRangeSelection = () => {
  56. return <LoadingError message={t('Trace view requires a date range selection.')} />;
  57. };
  58. const renderTraceHeader = (traceInfo: TraceInfo) => {
  59. const {meta} = props;
  60. const errors = meta?.errors ?? traceInfo.errors.size;
  61. const performanceIssues =
  62. meta?.performance_issues ?? traceInfo.performanceIssues.size;
  63. return (
  64. <TraceDetailHeader>
  65. <GuideAnchor target="trace_view_guide_breakdown">
  66. <MetaData
  67. headingText={t('Event Breakdown')}
  68. tooltipText={t(
  69. 'The number of transactions and issues there are in this trace.'
  70. )}
  71. bodyText={tct('[transactions] | [errors]', {
  72. transactions: tn(
  73. '%s Transaction',
  74. '%s Transactions',
  75. meta?.transactions ?? traceInfo.transactions.size
  76. ),
  77. errors: tn('%s Issue', '%s Issues', errors + performanceIssues),
  78. })}
  79. subtext={tn(
  80. 'Across %s project',
  81. 'Across %s projects',
  82. meta?.projects ?? traceInfo.projects.size
  83. )}
  84. />
  85. </GuideAnchor>
  86. <MetaData
  87. headingText={t('Total Duration')}
  88. tooltipText={t('The time elapsed between the start and end of this trace.')}
  89. bodyText={getDuration(
  90. traceInfo.endTimestamp - traceInfo.startTimestamp,
  91. 2,
  92. true
  93. )}
  94. subtext={getDynamicText({
  95. value: <TimeSince date={(traceInfo.endTimestamp || 0) * 1000} />,
  96. fixed: '5 days ago',
  97. })}
  98. />
  99. </TraceDetailHeader>
  100. );
  101. };
  102. const renderTraceWarnings = () => {
  103. const {traces, orphanErrors} = props;
  104. const {roots, orphans} = (traces ?? []).reduce(
  105. (counts, trace) => {
  106. if (isRootTransaction(trace)) {
  107. counts.roots++;
  108. } else {
  109. counts.orphans++;
  110. }
  111. return counts;
  112. },
  113. {roots: 0, orphans: 0}
  114. );
  115. let warning: React.ReactNode = null;
  116. if (roots === 0 && orphans > 0) {
  117. warning = (
  118. <Alert type="info" showIcon>
  119. <ExternalLink href="https://docs.sentry.io/product/performance/trace-view/#orphan-traces-and-broken-subtraces">
  120. {t(
  121. 'A root transaction is missing. Transactions linked by a dashed line have been orphaned and cannot be directly linked to the root.'
  122. )}
  123. </ExternalLink>
  124. </Alert>
  125. );
  126. } else if (roots === 1 && orphans > 0) {
  127. warning = (
  128. <Alert type="info" showIcon>
  129. <ExternalLink href="https://docs.sentry.io/product/performance/trace-view/#orphan-traces-and-broken-subtraces">
  130. {t(
  131. 'This trace has broken subtraces. Transactions linked by a dashed line have been orphaned and cannot be directly linked to the root.'
  132. )}
  133. </ExternalLink>
  134. </Alert>
  135. );
  136. } else if (roots > 1) {
  137. warning = (
  138. <Alert type="info" showIcon>
  139. <ExternalLink href="https://docs.sentry.io/product/sentry-basics/tracing/trace-view/#multiple-roots">
  140. {t('Multiple root transactions have been found with this trace ID.')}
  141. </ExternalLink>
  142. </Alert>
  143. );
  144. } else if (orphanErrors && orphanErrors.length > 1) {
  145. warning = (
  146. <Alert type="info" showIcon>
  147. {tct(
  148. "The good news is we know these errors are related to each other. The bad news is that we can't tell you more than that. If you haven't already, [tracingLink: configure performance monitoring for your SDKs] to learn more about service interactions.",
  149. {
  150. tracingLink: (
  151. <ExternalLink href="https://docs.sentry.io/product/performance/getting-started/" />
  152. ),
  153. }
  154. )}
  155. </Alert>
  156. );
  157. }
  158. return warning;
  159. };
  160. const renderContent = () => {
  161. const {
  162. dateSelected,
  163. isLoading,
  164. error,
  165. organization,
  166. location,
  167. traceEventView,
  168. traceSlug,
  169. traces,
  170. meta,
  171. orphanErrors,
  172. } = props;
  173. if (!dateSelected) {
  174. return renderTraceRequiresDateRangeSelection();
  175. }
  176. if (isLoading) {
  177. return renderTraceLoading();
  178. }
  179. const hasData = hasTraceData(traces, orphanErrors);
  180. if (error !== null || !hasData) {
  181. return (
  182. <TraceNotFound
  183. meta={meta}
  184. traceEventView={traceEventView}
  185. traceSlug={traceSlug}
  186. location={location}
  187. organization={organization}
  188. />
  189. );
  190. }
  191. const traceInfo = traces ? getTraceInfo(traces, orphanErrors) : undefined;
  192. return (
  193. <Fragment>
  194. {renderTraceWarnings()}
  195. {traceInfo && renderTraceHeader(traceInfo)}
  196. <Margin>
  197. <VisuallyCompleteWithData id="PerformanceDetails-TraceView" hasData={hasData}>
  198. <TraceView
  199. traceInfo={traceInfo}
  200. location={location}
  201. organization={organization}
  202. traceEventView={traceEventView}
  203. traceSlug={traceSlug}
  204. traces={traces || []}
  205. meta={meta}
  206. orphanErrors={orphanErrors || []}
  207. handleLimitChange={props.handleLimitChange}
  208. />
  209. </VisuallyCompleteWithData>
  210. </Margin>
  211. </Fragment>
  212. );
  213. };
  214. const {organization, location, traceEventView, traceSlug} = props;
  215. return (
  216. <Fragment>
  217. <Layout.Header>
  218. <Layout.HeaderContent>
  219. <Breadcrumb
  220. organization={organization}
  221. location={location}
  222. traceSlug={traceSlug}
  223. />
  224. <Layout.Title data-test-id="trace-header">
  225. {t('Trace ID: %s', traceSlug)}
  226. </Layout.Title>
  227. </Layout.HeaderContent>
  228. <Layout.HeaderActions>
  229. <ButtonBar gap={1}>
  230. <DiscoverButton
  231. size="sm"
  232. to={traceEventView.getResultsViewUrlTarget(organization.slug)}
  233. onClick={() => {
  234. trackAnalytics('performance_views.trace_view.open_in_discover', {
  235. organization,
  236. });
  237. }}
  238. >
  239. {t('Open in Discover')}
  240. </DiscoverButton>
  241. </ButtonBar>
  242. </Layout.HeaderActions>
  243. </Layout.Header>
  244. <Layout.Body>
  245. <Layout.Main fullWidth>{renderContent()}</Layout.Main>
  246. </Layout.Body>
  247. </Fragment>
  248. );
  249. }
  250. const StyledLoadingIndicator = styled(LoadingIndicator)`
  251. margin-bottom: 0;
  252. `;
  253. const LoadingContainer = styled('div')`
  254. font-size: ${p => p.theme.fontSizeLarge};
  255. color: ${p => p.theme.subText};
  256. text-align: center;
  257. `;
  258. const Margin = styled('div')`
  259. margin-top: ${space(2)};
  260. `;
  261. export default NewTraceDetailsContent;