recommendations.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import styled from '@emotion/styled';
  2. import {Tooltip} from 'sentry/components/tooltip';
  3. import {IconFile} from 'sentry/icons';
  4. import {t} from 'sentry/locale';
  5. import {space} from 'sentry/styles/space';
  6. import {getDuration} from 'sentry/utils/formatters';
  7. import {useResourcesQuery} from 'sentry/views/performance/browser/resources/utils/useResourcesQuery';
  8. import type {WebVitals} from 'sentry/views/performance/browser/webVitals/utils/types';
  9. import {SpanMetricsField} from 'sentry/views/starfish/types';
  10. export function Recommendations({
  11. transaction,
  12. webVital,
  13. }: {
  14. transaction: string;
  15. webVital: WebVitals;
  16. }) {
  17. switch (webVital) {
  18. case 'lcp':
  19. return null;
  20. case 'fid':
  21. return null;
  22. case 'cls':
  23. return null;
  24. case 'fcp':
  25. return <FcpRecommendations transaction={transaction} />;
  26. case 'ttfb':
  27. return null;
  28. default:
  29. return null;
  30. }
  31. }
  32. function FcpRecommendations({transaction}: {transaction: string}) {
  33. const query = `transaction:"${transaction}" resource.render_blocking_status:blocking`;
  34. const {data, isLoading} = useResourcesQuery({
  35. query,
  36. sort: {field: `avg(${SpanMetricsField.SPAN_SELF_TIME})`, kind: 'desc'},
  37. defaultResourceTypes: ['resource.script', 'resource.css', 'resource.img'],
  38. limit: 7,
  39. referrer: 'api.performance.browser.web-vitals.fcp-recommendations',
  40. });
  41. if (isLoading || !data || data.length < 1) {
  42. return null;
  43. }
  44. return (
  45. <RecommendationsContainer>
  46. <RecommendationsHeader />
  47. <ul>
  48. <RecommendationSubHeader>
  49. {t('Eliminate render blocking resources')}
  50. </RecommendationSubHeader>
  51. <ResourceList>
  52. {data.map(
  53. ({
  54. 'span.op': op,
  55. 'span.description': description,
  56. 'avg(span.self_time)': duration,
  57. }) => {
  58. return (
  59. <ResourceListItem key={description}>
  60. <Flex>
  61. <ResourceDescription>
  62. <StyledTooltip title={description}>
  63. <ResourceType resourceType={op} />
  64. {description}
  65. </StyledTooltip>
  66. </ResourceDescription>
  67. <span>{getFormattedDuration(duration)}</span>
  68. </Flex>
  69. </ResourceListItem>
  70. );
  71. }
  72. )}
  73. </ResourceList>
  74. </ul>
  75. </RecommendationsContainer>
  76. );
  77. }
  78. function RecommendationsHeader() {
  79. return (
  80. <RecommendationsHeaderContainer>
  81. <b>{t('Recommendations')}</b>
  82. </RecommendationsHeaderContainer>
  83. );
  84. }
  85. function ResourceType({resourceType}: {resourceType: `resource.${string}`}) {
  86. switch (resourceType) {
  87. case 'resource.script':
  88. return (
  89. <b>
  90. <StyledIconFile size="xs" />
  91. {t('js')}
  92. {' \u2014 '}
  93. </b>
  94. );
  95. case 'resource.css':
  96. return (
  97. <b>
  98. <StyledIconFile size="xs" />
  99. {t('css')}
  100. {' \u2014 '}
  101. </b>
  102. );
  103. case 'resource.img':
  104. return (
  105. <b>
  106. <StyledIconFile size="xs" />
  107. {t('img')}
  108. {' \u2014 '}
  109. </b>
  110. );
  111. default:
  112. return null;
  113. }
  114. }
  115. const getFormattedDuration = (value: number | null) => {
  116. if (value === null) {
  117. return null;
  118. }
  119. if (value < 1000) {
  120. return getDuration(value / 1000, 0, true);
  121. }
  122. return getDuration(value / 1000, 2, true);
  123. };
  124. const StyledIconFile = styled(IconFile)`
  125. margin-right: ${space(0.5)};
  126. `;
  127. const RecommendationSubHeader = styled('li')`
  128. margin-bottom: ${space(1)};
  129. `;
  130. const RecommendationsHeaderContainer = styled('div')`
  131. margin-bottom: ${space(1)};
  132. font-size: ${p => p.theme.fontSizeExtraLarge};
  133. `;
  134. const ResourceList = styled('ul')`
  135. padding-left: ${space(1)};
  136. `;
  137. const ResourceListItem = styled('li')`
  138. margin-bottom: ${space(0.5)};
  139. list-style: none;
  140. white-space: nowrap;
  141. `;
  142. const ResourceDescription = styled('span')`
  143. overflow: hidden;
  144. text-overflow: ellipsis;
  145. white-space: nowrap;
  146. `;
  147. const Flex = styled('span')`
  148. display: flex;
  149. justify-content: space-between;
  150. gap: ${space(1)};
  151. `;
  152. const RecommendationsContainer = styled('div')`
  153. margin-bottom: ${space(4)};
  154. `;
  155. const StyledTooltip = styled(Tooltip)`
  156. overflow: hidden;
  157. text-overflow: ellipsis;
  158. white-space: nowrap;
  159. `;