traceFooter.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import {SectionHeading} from 'sentry/components/charts/styles';
  5. import EventVitals from 'sentry/components/events/eventVitals';
  6. import Panel from 'sentry/components/panels/panel';
  7. import Placeholder from 'sentry/components/placeholder';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import type {EventTransaction, Organization} from 'sentry/types';
  11. import {generateQueryWithTag} from 'sentry/utils';
  12. import type EventView from 'sentry/utils/discover/eventView';
  13. import {formatTagKey} from 'sentry/utils/discover/fields';
  14. import type {
  15. TraceFullDetailed,
  16. TraceSplitResults,
  17. } from 'sentry/utils/performance/quickTrace/types';
  18. import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
  19. import type {UseApiQueryResult} from 'sentry/utils/queryClient';
  20. import type RequestError from 'sentry/utils/requestError/requestError';
  21. import Tags from 'sentry/views/discover/tags';
  22. import {getTraceInfo} from '../traceDetails/utils';
  23. type TraceFooterProps = {
  24. location: Location;
  25. organization: Organization;
  26. rootEventResults: UseApiQueryResult<EventTransaction, RequestError>;
  27. traceEventView: EventView;
  28. traces: TraceSplitResults<TraceFullDetailed> | null;
  29. };
  30. function NoWebVitals() {
  31. return (
  32. <div style={{flex: 1}}>
  33. <SectionHeading>{t('WebVitals')}</SectionHeading>
  34. <WebVitalsWrapper>
  35. {[
  36. WEB_VITAL_DETAILS['measurements.cls'],
  37. WEB_VITAL_DETAILS['measurements.lcp'],
  38. WEB_VITAL_DETAILS['measurements.ttfb'],
  39. WEB_VITAL_DETAILS['measurements.fcp'],
  40. WEB_VITAL_DETAILS['measurements.fid'],
  41. ].map(detail => (
  42. <StyledPanel key={detail.name}>
  43. <div>{detail.name}</div>
  44. <div>{' \u2014 '}</div>
  45. </StyledPanel>
  46. ))}
  47. </WebVitalsWrapper>
  48. </div>
  49. );
  50. }
  51. function TraceFooterLoading() {
  52. return (
  53. <TraceFooterWrapper>
  54. <div style={{flex: 1}}>
  55. <SectionHeading>{t('WebVitals')}</SectionHeading>
  56. <Fragment>
  57. <StyledPlaceholderVital key="title-1" />
  58. <StyledPlaceholderVital key="title-2" />
  59. <StyledPlaceholderVital key="title-3" />
  60. <StyledPlaceholderVital key="title-4" />
  61. <StyledPlaceholderVital key="title-5" />
  62. </Fragment>
  63. </div>
  64. <div style={{flex: 1}}>
  65. <SectionHeading>{t('Tag Summary')}</SectionHeading>
  66. <Fragment>
  67. <StyledPlaceholderTagTitle key="title-1" />
  68. <StyledPlaceholderTag key="bar-1" />
  69. <StyledPlaceholderTagTitle key="title-2" />
  70. <StyledPlaceholderTag key="bar-2" />
  71. <StyledPlaceholderTagTitle key="title-3" />
  72. <StyledPlaceholderTag key="bar-3" />
  73. </Fragment>
  74. </div>
  75. </TraceFooterWrapper>
  76. );
  77. }
  78. export function TraceFooter(props: TraceFooterProps) {
  79. if (!props.traces) {
  80. return <TraceFooterLoading />;
  81. }
  82. const {data: rootEvent} = props.rootEventResults;
  83. const {transactions, orphan_errors} = props.traces;
  84. const traceInfo = getTraceInfo(transactions, orphan_errors);
  85. const orphanErrorsCount = traceInfo.trailingOrphansCount ?? 0;
  86. const transactionsCount = traceInfo.transactions.size ?? 0;
  87. const totalNumOfEvents = transactionsCount + orphanErrorsCount;
  88. const webVitals = Object.keys(rootEvent?.measurements ?? {})
  89. .filter(name => Boolean(WEB_VITAL_DETAILS[`measurements.${name}`]))
  90. .sort();
  91. return rootEvent ? (
  92. <TraceFooterWrapper>
  93. {webVitals.length > 0 ? (
  94. <div style={{flex: 1}}>
  95. <EventVitals event={rootEvent} />
  96. </div>
  97. ) : (
  98. <NoWebVitals />
  99. )}
  100. <div style={{flex: 1}}>
  101. <Tags
  102. generateUrl={(key: string, value: string) => {
  103. const url = props.traceEventView.getResultsViewUrlTarget(
  104. props.organization.slug,
  105. false
  106. );
  107. url.query = generateQueryWithTag(url.query, {
  108. key: formatTagKey(key),
  109. value,
  110. });
  111. return url;
  112. }}
  113. totalValues={totalNumOfEvents}
  114. eventView={props.traceEventView}
  115. organization={props.organization}
  116. location={props.location}
  117. />
  118. </div>
  119. </TraceFooterWrapper>
  120. ) : null;
  121. }
  122. const TraceFooterWrapper = styled('div')`
  123. display: flex;
  124. gap: ${space(2)};
  125. margin-top: ${space(2)};
  126. `;
  127. const StyledPlaceholderTag = styled(Placeholder)`
  128. border-radius: ${p => p.theme.borderRadius};
  129. height: 16px;
  130. margin-bottom: ${space(1.5)};
  131. `;
  132. const StyledPlaceholderTagTitle = styled(Placeholder)`
  133. width: 100px;
  134. height: 12px;
  135. margin-bottom: ${space(0.5)};
  136. `;
  137. const StyledPlaceholderVital = styled(StyledPlaceholderTagTitle)`
  138. width: 100%;
  139. height: 50px;
  140. margin-bottom: ${space(0.5)};
  141. `;
  142. const StyledPanel = styled(Panel)`
  143. padding: ${space(1)} ${space(1.5)};
  144. margin-bottom: ${space(1)};
  145. width: 100%;
  146. `;
  147. const WebVitalsWrapper = styled('div')`
  148. display: flex;
  149. align-items: center;
  150. flex-direction: column;
  151. `;