recommendations.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  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 {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. });
  40. if (isLoading || !data || data.length < 1) {
  41. return null;
  42. }
  43. return (
  44. <RecommendationsContainer>
  45. <RecommendationsHeader />
  46. <ul>
  47. <RecommendationSubHeader>
  48. {t('Eliminate render blocking resources')}
  49. </RecommendationSubHeader>
  50. <ResourceList>
  51. {data.map(
  52. ({
  53. 'span.op': op,
  54. 'span.description': description,
  55. 'avg(span.self_time)': duration,
  56. }) => {
  57. return (
  58. <ResourceListItem key={description}>
  59. <Flex>
  60. <ResourceDescription>
  61. <StyledTooltip title={description}>
  62. <ResourceType resourceType={op} />
  63. {description}
  64. </StyledTooltip>
  65. </ResourceDescription>
  66. <span>{getFormattedDuration(duration)}</span>
  67. </Flex>
  68. </ResourceListItem>
  69. );
  70. }
  71. )}
  72. </ResourceList>
  73. </ul>
  74. </RecommendationsContainer>
  75. );
  76. }
  77. function RecommendationsHeader() {
  78. return (
  79. <RecommendationsHeaderContainer>
  80. <b>{t('Recommendations')}</b>
  81. </RecommendationsHeaderContainer>
  82. );
  83. }
  84. function ResourceType({resourceType}: {resourceType: `resource.${string}`}) {
  85. switch (resourceType) {
  86. case 'resource.script':
  87. return (
  88. <b>
  89. <StyledIconFile size="xs" />
  90. {t('js')}
  91. {' \u2014 '}
  92. </b>
  93. );
  94. case 'resource.css':
  95. return (
  96. <b>
  97. <StyledIconFile size="xs" />
  98. {t('css')}
  99. {' \u2014 '}
  100. </b>
  101. );
  102. case 'resource.img':
  103. return (
  104. <b>
  105. <StyledIconFile size="xs" />
  106. {t('img')}
  107. {' \u2014 '}
  108. </b>
  109. );
  110. default:
  111. return null;
  112. }
  113. }
  114. const getFormattedDuration = (value: number | null) => {
  115. if (value === null) {
  116. return null;
  117. }
  118. if (value < 1000) {
  119. return getDuration(value / 1000, 0, true);
  120. }
  121. return getDuration(value / 1000, 2, true);
  122. };
  123. const StyledIconFile = styled(IconFile)`
  124. margin-right: ${space(0.5)};
  125. `;
  126. const RecommendationSubHeader = styled('li')`
  127. margin-bottom: ${space(1)};
  128. `;
  129. const RecommendationsHeaderContainer = styled('div')`
  130. margin-bottom: ${space(1)};
  131. font-size: ${p => p.theme.fontSizeExtraLarge};
  132. `;
  133. const ResourceList = styled('ul')`
  134. padding-left: ${space(1)};
  135. `;
  136. const ResourceListItem = styled('li')`
  137. margin-bottom: ${space(0.5)};
  138. list-style: none;
  139. white-space: nowrap;
  140. `;
  141. const ResourceDescription = styled('span')`
  142. overflow: hidden;
  143. text-overflow: ellipsis;
  144. white-space: nowrap;
  145. `;
  146. const Flex = styled('span')`
  147. display: flex;
  148. justify-content: space-between;
  149. gap: ${space(1)};
  150. `;
  151. const RecommendationsContainer = styled('div')`
  152. margin-bottom: ${space(4)};
  153. `;
  154. const StyledTooltip = styled(Tooltip)`
  155. overflow: hidden;
  156. text-overflow: ellipsis;
  157. white-space: nowrap;
  158. `;