Browse Source

feat(starfish): Web Vitals slideout updates (#57231)

Updates webvitals slideout design. Also adds an opportunities column
which shows potential score increase.
edwardgou-sentry 1 year ago
parent
commit
bd7b6f0e18

+ 14 - 0
static/app/views/performance/browser/webVitals/utils/calculateOpportunity.tsx

@@ -0,0 +1,14 @@
+// Calculate the potential opportunity to increase projectScore by increasing transactionScore to 100
+export const calculateOpportunity = (
+  projectScore: number,
+  totalCount: number,
+  transactionScore: number,
+  transactionCount: number
+) => {
+  const cumulativeProjectScore = projectScore * totalCount;
+  const cumulativeComplementScore = (100 - transactionScore) * transactionCount;
+  const cumulativeNewProjectScore = cumulativeProjectScore + cumulativeComplementScore;
+  const newProjectScore = cumulativeNewProjectScore / totalCount;
+  const opportunity = newProjectScore - projectScore;
+  return Math.round(opportunity * 100) / 100;
+};

+ 8 - 1
static/app/views/performance/browser/webVitals/utils/types.tsx

@@ -9,6 +9,13 @@ export type Row = {
   'transaction.op': string;
   'transaction.op': string;
 };
 };
 
 
-export type RowWithScore = Row & {score: number};
+export type RowWithScore = Row & {
+  clsScore: number;
+  fcpScore: number;
+  fidScore: number;
+  lcpScore: number;
+  score: number;
+  ttfbScore: number;
+};
 
 
 export type WebVitals = 'lcp' | 'fcp' | 'cls' | 'ttfb' | 'fid';
 export type WebVitals = 'lcp' | 'fcp' | 'cls' | 'ttfb' | 'fid';

+ 2 - 3
static/app/views/performance/browser/webVitals/utils/useProjectWebVitalsQuery.tsx

@@ -4,9 +4,7 @@ import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
 import usePageFilters from 'sentry/utils/usePageFilters';
 import usePageFilters from 'sentry/utils/usePageFilters';
 
 
-type Props = {};
-
-export const useProjectWebVitalsQuery = ({}: Props) => {
+export const useProjectWebVitalsQuery = () => {
   const organization = useOrganization();
   const organization = useOrganization();
   const pageFilters = usePageFilters();
   const pageFilters = usePageFilters();
   const location = useLocation();
   const location = useLocation();
@@ -19,6 +17,7 @@ export const useProjectWebVitalsQuery = ({}: Props) => {
         'p75(measurements.cls)',
         'p75(measurements.cls)',
         'p75(measurements.ttfb)',
         'p75(measurements.ttfb)',
         'p75(measurements.fid)',
         'p75(measurements.fid)',
+        'count()',
       ],
       ],
       name: 'Web Vitals',
       name: 'Web Vitals',
       query:
       query:

+ 101 - 0
static/app/views/performance/browser/webVitals/utils/useProjectWebVitalsValuesTimeseriesQuery.tsx

@@ -0,0 +1,101 @@
+import {getInterval} from 'sentry/components/charts/utils';
+import {SeriesDataUnit} from 'sentry/types/echarts';
+import EventView, {MetaType} from 'sentry/utils/discover/eventView';
+import {
+  DiscoverQueryProps,
+  useGenericDiscoverQuery,
+} from 'sentry/utils/discover/genericDiscoverQuery';
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
+import usePageFilters from 'sentry/utils/usePageFilters';
+
+export const useProjectWebVitalsValuesTimeseriesQuery = () => {
+  const pageFilters = usePageFilters();
+  const location = useLocation();
+  const organization = useOrganization();
+  const projectTimeSeriesEventView = EventView.fromNewQueryWithPageFilters(
+    {
+      yAxis: [
+        'p75(measurements.lcp)',
+        'p75(measurements.fcp)',
+        'p75(measurements.cls)',
+        'p75(measurements.ttfb)',
+        'p75(measurements.fid)',
+      ],
+      name: 'Web Vitals',
+      query:
+        'transaction.op:pageload (transaction:/performance* or transaction:/discover* or transaction:/dashboards*)',
+      version: 2,
+      fields: [],
+      interval: getInterval(pageFilters.selection.datetime, 'low'),
+    },
+    pageFilters.selection
+  );
+
+  const result = useGenericDiscoverQuery<
+    {
+      data: any[];
+      meta: MetaType;
+    },
+    DiscoverQueryProps
+  >({
+    route: 'events-stats',
+    eventView: projectTimeSeriesEventView,
+    location,
+    orgSlug: organization.slug,
+    getRequestPayload: () => ({
+      ...projectTimeSeriesEventView.getEventsAPIPayload(location),
+      yAxis: projectTimeSeriesEventView.yAxis,
+      topEvents: projectTimeSeriesEventView.topEvents,
+      excludeOther: 0,
+      partial: 1,
+      orderby: undefined,
+      interval: projectTimeSeriesEventView.interval,
+    }),
+    options: {
+      enabled: pageFilters.isReady,
+      refetchOnWindowFocus: false,
+    },
+  });
+
+  const data: {
+    cls: SeriesDataUnit[];
+    fcp: SeriesDataUnit[];
+    fid: SeriesDataUnit[];
+    lcp: SeriesDataUnit[];
+    total: SeriesDataUnit[];
+    ttfb: SeriesDataUnit[];
+  } = {
+    lcp: [],
+    fcp: [],
+    cls: [],
+    ttfb: [],
+    fid: [],
+    total: [],
+  };
+
+  result?.data?.['p75(measurements.lcp)'].data.forEach((interval, index) => {
+    data.cls.push({
+      value: result?.data?.['p75(measurements.cls)'].data[index][1][0].count,
+      name: interval[0] * 1000,
+    });
+    data.lcp.push({
+      value: result?.data?.['p75(measurements.lcp)'].data[index][1][0].count,
+      name: interval[0] * 1000,
+    });
+    data.fcp.push({
+      value: result?.data?.['p75(measurements.fcp)'].data[index][1][0].count,
+      name: interval[0] * 1000,
+    });
+    data.ttfb.push({
+      value: result?.data?.['p75(measurements.ttfb)'].data[index][1][0].count,
+      name: interval[0] * 1000,
+    });
+    data.fid.push({
+      value: result?.data?.['p75(measurements.fid)'].data[index][1][0].count,
+      name: interval[0] * 1000,
+    });
+  });
+
+  return {data, isLoading: result.isLoading};
+};

+ 0 - 19
static/app/views/performance/browser/webVitals/webVitalsDescriptions/cls.tsx

@@ -1,19 +0,0 @@
-import {t} from 'sentry/locale';
-
-export function ClsDescription() {
-  return (
-    <div>
-      <h3>{t('Cumulative Layout Shift (CLS)')}</h3>
-      <p>
-        {t(
-          `Cumulative Layout Shift (CLS) is a stable Core Web Vital metric. It is an important, user-centric metric for measuring visual stability because it helps quantify how often users experience unexpected layout shifts—a low CLS helps ensure that the page is delightful.`
-        )}
-      </p>
-      <p>
-        <a href="https://web.dev/cls/" target="_blank" rel="noreferrer">
-          {t('Learn more about CLS')}
-        </a>
-      </p>
-    </div>
-  );
-}

+ 0 - 19
static/app/views/performance/browser/webVitals/webVitalsDescriptions/fcp.tsx

@@ -1,19 +0,0 @@
-import {t} from 'sentry/locale';
-
-export function FcpDescription() {
-  return (
-    <div>
-      <h3>{t('First Contentful Paint (FCP)')}</h3>
-      <p>
-        {t(
-          `First Contentful Paint (FCP) is an important, user-centric metric for measuring perceived load speed because it marks the first point in the page load timeline where the user can see anything on the screen—a fast FCP helps reassure the user that something is happening.`
-        )}
-      </p>
-      <p>
-        <a href="https://web.dev/fcp/" target="_blank" rel="noreferrer">
-          {t('Learn more about FCP')}
-        </a>
-      </p>
-    </div>
-  );
-}

+ 0 - 19
static/app/views/performance/browser/webVitals/webVitalsDescriptions/fid.tsx

@@ -1,19 +0,0 @@
-import {t} from 'sentry/locale';
-
-export function FidDescription() {
-  return (
-    <div>
-      <h3>{t('First Input Delay (FID)')}</h3>
-      <p>
-        {t(
-          `First Input Delay (FID) is the stable Core Web Vital metric for measuring load responsiveness because it quantifies the experience users feel when trying to interact with unresponsive pages—a low FID helps ensure that the page is usable. FID will be replaced by Interaction to Next Paint (INP) as a Core Web Vital in March 2024.`
-        )}
-      </p>
-      <p>
-        <a href="https://web.dev/fid/" target="_blank" rel="noreferrer">
-          {t('Learn more about FID')}
-        </a>
-      </p>
-    </div>
-  );
-}

+ 0 - 19
static/app/views/performance/browser/webVitals/webVitalsDescriptions/lcp.tsx

@@ -1,19 +0,0 @@
-import {t} from 'sentry/locale';
-
-export function LcpDescription() {
-  return (
-    <div>
-      <h3>{t('Largest Contentful Paint (LCP)')}</h3>
-      <p>
-        {t(
-          `Largest Contentful Paint (LCP) is an important, stable Core Web Vital metric for measuring perceived load speed because it marks the point in the page load timeline when the page's main content has likely loaded—a fast LCP helps reassure the user that the page is useful.`
-        )}
-      </p>
-      <p>
-        <a href="https://web.dev/lcp/" target="_blank" rel="noreferrer">
-          {t('Learn more about LCP')}
-        </a>
-      </p>
-    </div>
-  );
-}

+ 0 - 19
static/app/views/performance/browser/webVitals/webVitalsDescriptions/ttfb.tsx

@@ -1,19 +0,0 @@
-import {t} from 'sentry/locale';
-
-export function TtfbDescription() {
-  return (
-    <div>
-      <h3>{t('Time To First Byte (TTFB)')}</h3>
-      <p>
-        {t(
-          `Time to First Byte (TTFB) is a foundational metric for measuring connection setup time and web server responsiveness in both the lab and the field. It helps identify when a web server is too slow to respond to requests. In the case of navigation requests—that is, requests for an HTML document—it precedes every other meaningful loading performance metric.`
-        )}
-      </p>
-      <p>
-        <a href="https://web.dev/ttfb/" target="_blank" rel="noreferrer">
-          {t('Learn more about TTFB')}
-        </a>
-      </p>
-    </div>
-  );
-}

+ 117 - 0
static/app/views/performance/browser/webVitals/webVitalsDescriptions/webVitalDescription.tsx

@@ -0,0 +1,117 @@
+import {useTheme} from '@emotion/react';
+import styled from '@emotion/styled';
+
+import ProgressRing from 'sentry/components/progressRing';
+import {IconCheckmark} from 'sentry/icons/iconCheckmark';
+import {IconClose} from 'sentry/icons/iconClose';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {WebVital} from 'sentry/utils/fields';
+import {Browser} from 'sentry/utils/performance/vitals/constants';
+import {getScoreColor} from 'sentry/views/performance/browser/webVitals/utils/getScoreColor';
+import {WebVitals} from 'sentry/views/performance/browser/webVitals/utils/types';
+import {
+  vitalDescription,
+  vitalSupportedBrowsers,
+} from 'sentry/views/performance/vitalDetail/utils';
+
+type Props = {
+  score: number;
+  value: string;
+  webVital: WebVitals;
+};
+
+const webVitalFullNameMap = {
+  cls: t('Cumulative Layout Shift'),
+  fcp: t('First Contentful Paint'),
+  fid: t('First Input Delay'),
+  lcp: t('Largest Contentful Paint'),
+  ttfb: t('Time to First Byte'),
+};
+
+export function WebVitalDescription({score, value, webVital}: Props) {
+  const theme = useTheme();
+  return (
+    <div>
+      <Header>
+        <span>
+          <WebVitalName>{`${webVitalFullNameMap[webVital]} (P75)`}</WebVitalName>
+          <Value>{value}</Value>
+        </span>
+        <ProgressRing
+          value={score}
+          size={100}
+          barWidth={16}
+          text={
+            <ProgressRingTextContainer>
+              <ProgressRingText>{score}</ProgressRingText>
+              <ProgressRingSubText>{webVital.toUpperCase()}</ProgressRingSubText>
+            </ProgressRingTextContainer>
+          }
+          progressColor={getScoreColor(score, theme)}
+          backgroundColor={`${getScoreColor(score, theme)}33`}
+        />
+      </Header>
+      <p>{vitalDescription[WebVital[webVital.toUpperCase()]]}</p>
+      <SupportedBrowsers>
+        {Object.values(Browser).map(browser => (
+          <BrowserItem key={browser}>
+            {vitalSupportedBrowsers[WebVital[webVital.toUpperCase()]]?.includes(
+              browser
+            ) ? (
+              <IconCheckmark color="successText" size="sm" />
+            ) : (
+              <IconClose color="dangerText" size="sm" />
+            )}
+            {browser}
+          </BrowserItem>
+        ))}
+      </SupportedBrowsers>
+    </div>
+  );
+}
+
+const SupportedBrowsers = styled('div')`
+  display: inline-flex;
+  gap: ${space(2)};
+  margin-bottom: ${space(3)};
+`;
+
+const BrowserItem = styled('div')`
+  display: flex;
+  align-items: center;
+  gap: ${space(1)};
+`;
+
+const Header = styled('span')`
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: ${space(3)};
+`;
+
+const Value = styled('h2')`
+  font-weight: normal;
+  margin-bottom: ${space(1)};
+`;
+
+const WebVitalName = styled('h4')`
+  margin-bottom: ${space(1)};
+  margin-top: 40px;
+`;
+
+const ProgressRingTextContainer = styled('div')`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+`;
+
+const ProgressRingText = styled('h4')`
+  color: ${p => p.theme.textColor};
+  margin: ${space(2)} 0 0 0;
+`;
+
+const ProgressRingSubText = styled('h5')`
+  font-size: ${p => p.theme.fontSizeSmall};
+  color: ${p => p.theme.textColor};
+`;

Some files were not shown because too many files changed in this diff