|
@@ -0,0 +1,173 @@
|
|
|
+import styled from '@emotion/styled';
|
|
|
+
|
|
|
+import {Tooltip} from 'sentry/components/tooltip';
|
|
|
+import {IconFile} from 'sentry/icons';
|
|
|
+import {t} from 'sentry/locale';
|
|
|
+import {space} from 'sentry/styles/space';
|
|
|
+import {getDuration} from 'sentry/utils/formatters';
|
|
|
+import {useResourcesQuery} from 'sentry/views/performance/browser/resources/utils/useResourcesQuery';
|
|
|
+import {WebVitals} from 'sentry/views/performance/browser/webVitals/utils/types';
|
|
|
+import {SpanMetricsField} from 'sentry/views/starfish/types';
|
|
|
+
|
|
|
+export function Recommendations({
|
|
|
+ transaction,
|
|
|
+ webVital,
|
|
|
+}: {
|
|
|
+ transaction: string;
|
|
|
+ webVital: WebVitals;
|
|
|
+}) {
|
|
|
+ switch (webVital) {
|
|
|
+ case 'lcp':
|
|
|
+ return null;
|
|
|
+ case 'fid':
|
|
|
+ return null;
|
|
|
+ case 'cls':
|
|
|
+ return null;
|
|
|
+ case 'fcp':
|
|
|
+ return <FcpRecommendations transaction={transaction} />;
|
|
|
+ case 'ttfb':
|
|
|
+ return null;
|
|
|
+ default:
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function FcpRecommendations({transaction}: {transaction: string}) {
|
|
|
+ const query = `transaction:"${transaction}" resource.render_blocking_status:blocking`;
|
|
|
+ const {data, isLoading} = useResourcesQuery({
|
|
|
+ query,
|
|
|
+ sort: {field: `avg(${SpanMetricsField.SPAN_SELF_TIME})`, kind: 'desc'},
|
|
|
+ defaultResourceTypes: ['resource.script', 'resource.css', 'resource.img'],
|
|
|
+ limit: 7,
|
|
|
+ });
|
|
|
+ if (isLoading || !data) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return (
|
|
|
+ <RecommendationsContainer>
|
|
|
+ <RecommendationsHeader />
|
|
|
+ <ul>
|
|
|
+ <RecommendationSubHeader>
|
|
|
+ {t('Eliminate render blocking resources')}
|
|
|
+ </RecommendationSubHeader>
|
|
|
+ <ResourceList>
|
|
|
+ {data.map(
|
|
|
+ ({
|
|
|
+ 'span.op': op,
|
|
|
+ 'span.description': description,
|
|
|
+ 'avg(span.self_time)': duration,
|
|
|
+ }) => {
|
|
|
+ return (
|
|
|
+ <ResourceListItem key={description}>
|
|
|
+ <Flex>
|
|
|
+ <ResourceDescription>
|
|
|
+ <StyledTooltip title={description}>
|
|
|
+ <ResourceType resourceType={op} />
|
|
|
+ {description}
|
|
|
+ </StyledTooltip>
|
|
|
+ </ResourceDescription>
|
|
|
+ <span>{getFormattedDuration(duration)}</span>
|
|
|
+ </Flex>
|
|
|
+ </ResourceListItem>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ )}
|
|
|
+ </ResourceList>
|
|
|
+ </ul>
|
|
|
+ </RecommendationsContainer>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function RecommendationsHeader() {
|
|
|
+ return (
|
|
|
+ <RecommendationsHeaderContainer>
|
|
|
+ <b>{t('Recommendations')}</b>
|
|
|
+ </RecommendationsHeaderContainer>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function ResourceType({resourceType}: {resourceType: `resource.${string}`}) {
|
|
|
+ switch (resourceType) {
|
|
|
+ case 'resource.script':
|
|
|
+ return (
|
|
|
+ <b>
|
|
|
+ <StyledIconFile size="xs" />
|
|
|
+ {t('js')}
|
|
|
+ {' \u2014 '}
|
|
|
+ </b>
|
|
|
+ );
|
|
|
+ case 'resource.css':
|
|
|
+ return (
|
|
|
+ <b>
|
|
|
+ <StyledIconFile size="xs" />
|
|
|
+ {t('css')}
|
|
|
+ {' \u2014 '}
|
|
|
+ </b>
|
|
|
+ );
|
|
|
+ case 'resource.img':
|
|
|
+ return (
|
|
|
+ <b>
|
|
|
+ <StyledIconFile size="xs" />
|
|
|
+ {t('img')}
|
|
|
+ {' \u2014 '}
|
|
|
+ </b>
|
|
|
+ );
|
|
|
+ default:
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const getFormattedDuration = (value: number | null) => {
|
|
|
+ if (value === null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (value < 1000) {
|
|
|
+ return getDuration(value / 1000, 0, true);
|
|
|
+ }
|
|
|
+ return getDuration(value / 1000, 2, true);
|
|
|
+};
|
|
|
+
|
|
|
+const StyledIconFile = styled(IconFile)`
|
|
|
+ margin-right: ${space(0.5)};
|
|
|
+`;
|
|
|
+
|
|
|
+const RecommendationSubHeader = styled('li')`
|
|
|
+ margin-bottom: ${space(1)};
|
|
|
+`;
|
|
|
+
|
|
|
+const RecommendationsHeaderContainer = styled('div')`
|
|
|
+ margin-bottom: ${space(1)};
|
|
|
+ font-size: ${p => p.theme.fontSizeExtraLarge};
|
|
|
+`;
|
|
|
+
|
|
|
+const ResourceList = styled('ul')`
|
|
|
+ padding-left: ${space(1)};
|
|
|
+`;
|
|
|
+
|
|
|
+const ResourceListItem = styled('li')`
|
|
|
+ margin-bottom: ${space(0.5)};
|
|
|
+ list-style: none;
|
|
|
+ white-space: nowrap;
|
|
|
+`;
|
|
|
+
|
|
|
+const ResourceDescription = styled('span')`
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+`;
|
|
|
+
|
|
|
+const Flex = styled('span')`
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: ${space(1)};
|
|
|
+`;
|
|
|
+
|
|
|
+const RecommendationsContainer = styled('div')`
|
|
|
+ margin-bottom: ${space(4)};
|
|
|
+`;
|
|
|
+
|
|
|
+const StyledTooltip = styled(Tooltip)`
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+`;
|