recommendations.tsx 4.2 KB

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