Browse Source

feat(webvitals): add support for inp in webvitals meters and slideout detail panel (#64229)

Replace FID webvital meter on the landing page with INP. Also update the
slideout panel to support showing INP data.
edwardgou-sentry 1 year ago
parent
commit
8f91d4fdff

+ 8 - 1
static/app/views/performance/browser/webVitals/components/webVitalDescription.tsx

@@ -12,6 +12,10 @@ import type {Tag} from 'sentry/types';
 import {WebVital} from 'sentry/utils/fields';
 import {Browser} from 'sentry/utils/performance/vitals/constants';
 import {Dot} from 'sentry/views/performance/browser/webVitals/components/webVitalMeters';
+import {
+  ORDER,
+  ORDER_WITH_INP,
+} from 'sentry/views/performance/browser/webVitals/performanceScoreChart';
 import {PERFORMANCE_SCORE_COLORS} from 'sentry/views/performance/browser/webVitals/utils/performanceScoreColors';
 import {
   scoreToStatus,
@@ -21,6 +25,7 @@ import type {
   ProjectScore,
   WebVitals,
 } from 'sentry/views/performance/browser/webVitals/utils/types';
+import {useReplaceFidWithInpSetting} from 'sentry/views/performance/browser/webVitals/utils/useReplaceFidWithInpSetting';
 import {vitalSupportedBrowsers} from 'sentry/views/performance/vitalDetail/utils';
 
 import PerformanceScoreRingWithTooltips from './performanceScoreRingWithTooltips';
@@ -69,9 +74,11 @@ type WebVitalDetailHeaderProps = {
 };
 
 export function WebVitalDetailHeader({score, value, webVital}: Props) {
+  const shouldReplaceFidWithInp = useReplaceFidWithInpSetting();
   const theme = useTheme();
   const colors = theme.charts.getColorPalette(3);
-  const dotColor = colors[['lcp', 'fcp', 'fid', 'cls', 'ttfb'].indexOf(webVital)];
+  const dotColor =
+    colors[(shouldReplaceFidWithInp ? ORDER_WITH_INP : ORDER).indexOf(webVital)];
   const status = score !== undefined ? scoreToStatus(score) : undefined;
 
   return (

+ 80 - 0
static/app/views/performance/browser/webVitals/components/webVitalMeters.spec.tsx

@@ -0,0 +1,80 @@
+import {OrganizationFixture} from 'sentry-fixture/organization';
+
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import type {TableData} from 'sentry/utils/discover/discoverQuery';
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import WebVitalMeters from 'sentry/views/performance/browser/webVitals/components/webVitalMeters';
+import type {ProjectScore} from 'sentry/views/performance/browser/webVitals/utils/types';
+
+jest.mock('sentry/utils/useLocation');
+jest.mock('sentry/utils/usePageFilters');
+jest.mock('sentry/utils/useOrganization');
+
+describe('WebVitalMeters', function () {
+  const organization = OrganizationFixture();
+  const projectScore: ProjectScore = {
+    lcpWeight: 30,
+    fcpWeight: 20,
+    fidWeight: 25,
+    clsWeight: 15,
+    ttfbWeight: 10,
+    inpWeight: 10,
+  };
+  const projectData: TableData = {
+    data: [],
+  };
+
+  beforeEach(function () {
+    jest.mocked(useLocation).mockReturnValue({
+      pathname: '',
+      search: '',
+      query: {},
+      hash: '',
+      state: undefined,
+      action: 'PUSH',
+      key: '',
+    });
+    jest.mocked(usePageFilters).mockReturnValue({
+      isReady: true,
+      desyncedFilters: new Set(),
+      pinnedFilters: new Set(),
+      shouldPersist: true,
+      selection: {
+        datetime: {
+          period: '10d',
+          start: null,
+          end: null,
+          utc: false,
+        },
+        environments: [],
+        projects: [],
+      },
+    });
+    jest.mocked(useOrganization).mockReturnValue(organization);
+  });
+
+  it('renders web vital meters with first input delay', async () => {
+    render(<WebVitalMeters projectData={projectData} projectScore={projectScore} />);
+    await screen.findByText('Largest Contentful Paint');
+    screen.getByText('First Contentful Paint');
+    screen.getByText('Cumulative Layout Shift');
+    screen.getByText('Time To First Byte');
+    screen.getByText('First Input Delay');
+  });
+
+  it('renders web vital meters with interaction to next paint', async () => {
+    const organizationWithInp = OrganizationFixture({
+      features: ['starfish-browser-webvitals-replace-fid-with-inp'],
+    });
+    jest.mocked(useOrganization).mockReturnValue(organizationWithInp);
+    render(<WebVitalMeters projectData={projectData} projectScore={projectScore} />);
+    await screen.findByText('Largest Contentful Paint');
+    screen.getByText('First Contentful Paint');
+    screen.getByText('Cumulative Layout Shift');
+    screen.getByText('Time To First Byte');
+    screen.getByText('Interaction to Next Paint');
+  });
+});

+ 20 - 3
static/app/views/performance/browser/webVitals/components/webVitalMeters.tsx

@@ -19,6 +19,7 @@ import type {
   ProjectScore,
   WebVitals,
 } from 'sentry/views/performance/browser/webVitals/utils/types';
+import {useReplaceFidWithInpSetting} from 'sentry/views/performance/browser/webVitals/utils/useReplaceFidWithInpSetting';
 
 type Props = {
   onClick?: (webVital: WebVitals) => void;
@@ -51,6 +52,17 @@ const WEB_VITALS_METERS_CONFIG = {
   },
 };
 
+const WEB_VITALS_METERS_CONFIG_WITH_INP = {
+  lcp: WEB_VITALS_METERS_CONFIG.lcp,
+  fcp: WEB_VITALS_METERS_CONFIG.fcp,
+  inp: {
+    name: t('Interaction to Next Paint'),
+    formatter: (value: number) => getFormattedDuration(value / 1000),
+  },
+  cls: WEB_VITALS_METERS_CONFIG.cls,
+  ttfb: WEB_VITALS_METERS_CONFIG.ttfb,
+};
+
 export default function WebVitalMeters({
   onClick,
   projectData,
@@ -58,12 +70,17 @@ export default function WebVitalMeters({
   showTooltip = true,
 }: Props) {
   const theme = useTheme();
+  const shouldReplaceFidWithInp = useReplaceFidWithInpSetting();
 
   if (!projectScore) {
     return null;
   }
 
-  const webVitals = Object.keys(WEB_VITALS_METERS_CONFIG) as WebVitals[];
+  const webVitalsConfig = shouldReplaceFidWithInp
+    ? WEB_VITALS_METERS_CONFIG_WITH_INP
+    : WEB_VITALS_METERS_CONFIG;
+
+  const webVitals = Object.keys(webVitalsConfig) as WebVitals[];
   const colors = theme.charts.getColorPalette(3);
 
   return (
@@ -72,13 +89,13 @@ export default function WebVitalMeters({
         {webVitals.map((webVital, index) => {
           const webVitalExists = projectScore[`${webVital}Score`] !== undefined;
           const formattedMeterValueText = webVitalExists ? (
-            WEB_VITALS_METERS_CONFIG[webVital].formatter(
+            webVitalsConfig[webVital].formatter(
               projectData?.data?.[0]?.[`p75(measurements.${webVital})`] as number
             )
           ) : (
             <NoValue />
           );
-          const headerText = WEB_VITALS_METERS_CONFIG[webVital].name;
+          const headerText = webVitalsConfig[webVital].name;
           const meterBody = (
             <Fragment>
               <MeterBarBody>

+ 14 - 8
static/app/views/performance/browser/webVitals/webVitalsDetailPanel.tsx

@@ -31,6 +31,7 @@ import type {
   RowWithScoreAndOpportunity,
   WebVitals,
 } from 'sentry/views/performance/browser/webVitals/utils/types';
+import {useReplaceFidWithInpSetting} from 'sentry/views/performance/browser/webVitals/utils/useReplaceFidWithInpSetting';
 import {useStoredScoresSetting} from 'sentry/views/performance/browser/webVitals/utils/useStoredScoresSetting';
 import DetailPanel from 'sentry/views/starfish/components/detailPanel';
 
@@ -58,31 +59,33 @@ export function WebVitalsDetailPanel({
   const organization = useOrganization();
   const location = useLocation();
   const shouldUseStoredScores = useStoredScoresSetting();
+  const shouldReplaceFidWithInp = useReplaceFidWithInpSetting();
+  // TODO: Revert this when INP is queryable in discover.
+  const webVitalFilter = shouldReplaceFidWithInp && webVital === 'inp' ? 'fid' : webVital;
 
   const {data: projectData} = useProjectRawWebVitalsQuery({});
   const {data: projectScoresData} = useProjectWebVitalsScoresQuery({
     enabled: shouldUseStoredScores,
-    weightWebVital: webVital ?? 'total',
+    weightWebVital: webVitalFilter ?? 'total',
   });
 
   const projectScore = shouldUseStoredScores
     ? calculatePerformanceScoreFromStoredTableDataRow(projectScoresData?.data?.[0])
     : calculatePerformanceScoreFromTableDataRow(projectData?.data?.[0]);
-
   const {data, isLoading} = useTransactionWebVitalsQuery({
     limit: 100,
-    opportunityWebVital: webVital ?? 'total',
+    opportunityWebVital: webVitalFilter ?? 'total',
     ...(webVital
       ? shouldUseStoredScores
         ? {
-            query: `count_scores(measurements.score.${webVital}):>0`,
+            query: `count_scores(measurements.score.${webVitalFilter}):>0`,
             defaultSort: {
-              field: `opportunity_score(measurements.score.${webVital})`,
+              field: `opportunity_score(measurements.score.${webVitalFilter})`,
               kind: 'desc',
             },
           }
         : {
-            query: `count_web_vitals(measurements.${webVital},any):>0`,
+            query: `count_web_vitals(measurements.${webVitalFilter},any):>0`,
           }
       : {}),
     enabled: webVital !== null,
@@ -94,7 +97,7 @@ export function WebVitalsDetailPanel({
     }
     const count = projectData?.data?.[0]?.['count()'] as number;
     const sumWeights = projectScoresData?.data?.[0]?.[
-      `sum(measurements.score.weight.${webVital})`
+      `sum(measurements.score.weight.${webVitalFilter})`
     ] as number;
     return data
       .map(row => ({
@@ -128,6 +131,7 @@ export function WebVitalsDetailPanel({
     projectScoresData?.data,
     shouldUseStoredScores,
     webVital,
+    webVitalFilter,
   ]);
 
   const {data: timeseriesData, isLoading: isTimeseriesLoading} =
@@ -198,7 +202,7 @@ export function WebVitalsDetailPanel({
     }
     if (col.key === 'webVital') {
       let value: string | number = row[mapWebVitalToColumn(webVital)];
-      if (webVital && ['lcp', 'fcp', 'ttfb', 'fid'].includes(webVital)) {
+      if (webVital && ['lcp', 'fcp', 'ttfb', 'fid', 'inp'].includes(webVital)) {
         value = getFormattedDuration(value);
       } else if (webVital === 'cls') {
         value = value?.toFixed(2);
@@ -287,6 +291,8 @@ const mapWebVitalToColumn = (webVital?: WebVitals | null) => {
       return 'p75(measurements.ttfb)';
     case 'fid':
       return 'p75(measurements.fid)';
+    case 'inp':
+      return 'p75(measurements.inp)';
     default:
       return 'count()';
   }

+ 1 - 0
static/app/views/performance/vitalDetail/utils.tsx

@@ -194,6 +194,7 @@ export const vitalSupportedBrowsers: Partial<Record<WebVital, Browser[]>> = {
     Browser.SAFARI,
     Browser.IE,
   ],
+  [WebVital.INP]: [Browser.CHROME, Browser.EDGE, Browser.OPERA],
 };
 
 export function getVitalChartDefinitions({