Browse Source

feat(perf): Show supported browsers on vital details page (#31672)

This PR allows users to see which browsers offer support for the current Web Vital that they are viewing in its details page. This can clear up any confusion as to why web vitals data is not being received.
Ash Anand 3 years ago
parent
commit
0c0f3b3ef6

+ 1 - 1
static/app/icons/iconClose.tsx

@@ -9,7 +9,7 @@ interface Props extends SVGIconProps {
 const IconClose = React.forwardRef<SVGSVGElement, Props>(
   ({isCircled = false, ...props}, ref) => {
     return (
-      <SvgIcon ref={ref} {...props}>
+      <SvgIcon ref={ref} {...props} data-test-id="icon-close">
         {isCircled ? (
           <React.Fragment>
             <path d="M8,16a8,8,0,1,1,8-8A8,8,0,0,1,8,16ZM8,1.53A6.47,6.47,0,1,0,14.47,8,6.47,6.47,0,0,0,8,1.53Z" />

+ 9 - 0
static/app/utils/performance/vitals/constants.tsx

@@ -165,3 +165,12 @@ export const MOBILE_VITAL_DETAILS: Record<MobileVital, Vital> = {
     type: measurementType(MobileVital.StallPercentage),
   },
 };
+
+export enum Browser {
+  CHROME = 'Chrome',
+  EDGE = 'Edge',
+  OPERA = 'Opera',
+  FIREFOX = 'Firefox',
+  SAFARI = 'Safari',
+  IE = 'IE',
+}

+ 34 - 4
static/app/views/performance/vitalDetail/utils.tsx

@@ -9,6 +9,7 @@ import {Series} from 'sentry/types/echarts';
 import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts';
 import {getAggregateAlias, WebVital} from 'sentry/utils/discover/fields';
 import {TransactionMetric} from 'sentry/utils/metrics/fields';
+import {Browser} from 'sentry/utils/performance/vitals/constants';
 import {decodeScalar} from 'sentry/utils/queryString';
 import {Color, Theme} from 'sentry/utils/theme';
 
@@ -117,13 +118,13 @@ export const vitalChartTitleMap = vitalMap;
 
 export const vitalDescription: Partial<Record<WebVital, string>> = {
   [WebVital.FCP]:
-    'First Contentful Paint (FCP) measures the amount of time the first content takes to render in the viewport. Like FP, this could also show up in any form from the document object model (DOM), such as images, SVGs, or text blocks.',
+    'First Contentful Paint (FCP) measures the amount of time the first content takes to render in the viewport. Like FP, this could also show up in any form from the document object model (DOM), such as images, SVGs, or text blocks. At the moment, there is support for FCP in the following browsers:',
   [WebVital.CLS]:
-    'Cumulative Layout Shift (CLS) is the sum of individual layout shift scores for every unexpected element shift during the rendering process. Imagine navigating to an article and trying to click a link before the page finishes loading. Before your cursor even gets there, the link may have shifted down due to an image rendering. Rather than using duration for this Web Vital, the CLS score represents the degree of disruptive and visually unstable shifts.',
+    'Cumulative Layout Shift (CLS) is the sum of individual layout shift scores for every unexpected element shift during the rendering process. Imagine navigating to an article and trying to click a link before the page finishes loading. Before your cursor even gets there, the link may have shifted down due to an image rendering. Rather than using duration for this Web Vital, the CLS score represents the degree of disruptive and visually unstable shifts. At the moment, there is support for CLS in the following browsers:',
   [WebVital.FID]:
-    'First Input Delay measures the response time when the user tries to interact with the viewport. Actions maybe include clicking a button, link or other custom Javascript controller. It is key in helping the user determine if a page is usable or not.',
+    'First Input Delay (FID) measures the response time when the user tries to interact with the viewport. Actions maybe include clicking a button, link or other custom Javascript controller. It is key in helping the user determine if a page is usable or not. At the moment, there is support for FID in the following browsers:',
   [WebVital.LCP]:
-    'Largest Contentful Paint (LCP) measures the render time for the largest content to appear in the viewport. This may be in any form from the document object model (DOM), such as images, SVGs, or text blocks. It’s the largest pixel area in the viewport, thus most visually defining. LCP helps developers understand how long it takes to see the main content on the page.',
+    'Largest Contentful Paint (LCP) measures the render time for the largest content to appear in the viewport. This may be in any form from the document object model (DOM), such as images, SVGs, or text blocks. It’s the largest pixel area in the viewport, thus most visually defining. LCP helps developers understand how long it takes to see the main content on the page. At the moment, there is support for LCP in the following browsers:',
 };
 
 export const vitalAbbreviations: Partial<Record<WebVital, string>> = {
@@ -150,6 +151,35 @@ export const vitalToMetricsField: Record<string, TransactionMetric> = {
   [WebVital.CLS]: TransactionMetric.SENTRY_TRANSACTIONS_MEASUREMENTS_CLS,
 };
 
+export const vitalSupportedBrowsers: Partial<Record<WebVital, Browser[]>> = {
+  [WebVital.LCP]: [Browser.CHROME, Browser.EDGE, Browser.OPERA],
+  [WebVital.FID]: [
+    Browser.CHROME,
+    Browser.EDGE,
+    Browser.OPERA,
+    Browser.FIREFOX,
+    Browser.SAFARI,
+    Browser.IE,
+  ],
+  [WebVital.CLS]: [Browser.CHROME, Browser.EDGE, Browser.OPERA],
+  [WebVital.FP]: [Browser.CHROME, Browser.EDGE, Browser.OPERA],
+  [WebVital.FCP]: [
+    Browser.CHROME,
+    Browser.EDGE,
+    Browser.OPERA,
+    Browser.FIREFOX,
+    Browser.SAFARI,
+  ],
+  [WebVital.TTFB]: [
+    Browser.CHROME,
+    Browser.EDGE,
+    Browser.OPERA,
+    Browser.FIREFOX,
+    Browser.SAFARI,
+    Browser.IE,
+  ],
+};
+
 export function getVitalChartDefinitions({
   theme,
   location,

+ 32 - 2
static/app/views/performance/vitalDetail/vitalDetailContent.tsx

@@ -16,7 +16,7 @@ import * as Layout from 'sentry/components/layouts/thirds';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
 import * as TeamKeyTransactionManager from 'sentry/components/performance/teamKeyTransactionsManager';
-import {IconChevron} from 'sentry/icons';
+import {IconCheckmark, IconChevron, IconClose} from 'sentry/icons';
 import {IconFlag} from 'sentry/icons/iconFlag';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
@@ -26,6 +26,7 @@ import {getUtcToLocalDateObject} from 'sentry/utils/dates';
 import EventView from 'sentry/utils/discover/eventView';
 import {WebVital} from 'sentry/utils/discover/fields';
 import MetricsRequest from 'sentry/utils/metrics/metricsRequest';
+import {Browser} from 'sentry/utils/performance/vitals/constants';
 import {decodeScalar} from 'sentry/utils/queryString';
 import Teams from 'sentry/utils/teams';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
@@ -38,7 +39,12 @@ import {MetricsSwitch} from '../metricsSwitch';
 import {getTransactionSearchQuery} from '../utils';
 
 import Table from './table';
-import {vitalDescription, vitalMap, vitalToMetricsField} from './utils';
+import {
+  vitalDescription,
+  vitalMap,
+  vitalSupportedBrowsers,
+  vitalToMetricsField,
+} from './utils';
 import VitalChart from './vitalChart';
 import VitalChartMetrics from './vitalChartMetrics';
 import VitalInfo from './vitalInfo';
@@ -370,6 +376,18 @@ class VitalDetailContent extends Component<Props, State> {
           )}
           <Layout.Main fullWidth>
             <StyledDescription>{vitalDescription[vitalName]}</StyledDescription>
+            <SupportedBrowsers>
+              {Object.values(Browser).map(browser => (
+                <BrowserItem key={browser}>
+                  {vitalSupportedBrowsers[vitalName]?.includes(browser) ? (
+                    <IconCheckmark color="green200" size="sm" />
+                  ) : (
+                    <IconClose color="red300" size="sm" />
+                  )}
+                  {browser}
+                </BrowserItem>
+              ))}
+            </SupportedBrowsers>
             {this.renderContent(vital)}
           </Layout.Main>
         </Layout.Body>
@@ -396,3 +414,15 @@ const StyledVitalInfo = styled('div')`
 const StyledMetricsSearchBar = styled(MetricsSearchBar)`
   margin-bottom: ${space(2)};
 `;
+
+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)};
+`;

+ 81 - 1
tests/js/spec/views/performance/vitalDetail/index.spec.tsx

@@ -2,14 +2,17 @@ import {browserHistory, InjectedRouter} from 'react-router';
 
 import {enforceActOnUseLegacyStoreHook} from 'sentry-test/enzyme';
 import {initializeOrg} from 'sentry-test/initializeOrg';
-import {mountWithTheme, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+import {mountWithTheme, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
 import {textWithMarkupMatcher} from 'sentry-test/utils';
 
 import ProjectsStore from 'sentry/stores/projectsStore';
 import TeamStore from 'sentry/stores/teamStore';
+import {WebVital} from 'sentry/utils/discover/fields';
+import {Browser} from 'sentry/utils/performance/vitals/constants';
 import {OrganizationContext} from 'sentry/views/organizationContext';
 import {MetricsSwitchContext} from 'sentry/views/performance/metricsSwitch';
 import VitalDetail from 'sentry/views/performance/vitalDetail';
+import {vitalSupportedBrowsers} from 'sentry/views/performance/vitalDetail/utils';
 
 const api = new MockApiClient();
 const organization = TestStubs.Organization({
@@ -58,6 +61,21 @@ function TestComponent(
   );
 }
 
+const testSupportedBrowserRendering = (webVital: WebVital) => {
+  Object.values(Browser).forEach(browser => {
+    const browserElement = screen.getByText(browser);
+    expect(browserElement).toBeInTheDocument();
+
+    const isSupported = vitalSupportedBrowsers[webVital]?.includes(browser);
+
+    if (isSupported) {
+      expect(within(browserElement).getByTestId('icon-check-mark')).toBeInTheDocument();
+    } else {
+      expect(within(browserElement).getByTestId('icon-close')).toBeInTheDocument();
+    }
+  });
+};
+
 describe('Performance > VitalDetail', function () {
   enforceActOnUseLegacyStoreHook();
 
@@ -605,4 +623,66 @@ describe('Performance > VitalDetail', function () {
     // The table is still a TODO
     expect(screen.getByText('TODO')).toBeInTheDocument();
   });
+
+  it('correctly renders which browsers support LCP', async function () {
+    mountWithTheme(<TestComponent />, {
+      context: routerContext,
+    });
+
+    testSupportedBrowserRendering(WebVital.LCP);
+  });
+
+  it('correctly renders which browsers support CLS', async function () {
+    const newRouter = {
+      ...router,
+      location: {
+        ...router.location,
+        query: {
+          vitalName: 'measurements.cls',
+        },
+      },
+    };
+
+    mountWithTheme(<TestComponent router={newRouter} />, {
+      context: routerContext,
+    });
+
+    testSupportedBrowserRendering(WebVital.CLS);
+  });
+
+  it('correctly renders which browsers support FCP', async function () {
+    const newRouter = {
+      ...router,
+      location: {
+        ...router.location,
+        query: {
+          vitalName: 'measurements.fcp',
+        },
+      },
+    };
+
+    mountWithTheme(<TestComponent router={newRouter} />, {
+      context: routerContext,
+    });
+
+    testSupportedBrowserRendering(WebVital.FCP);
+  });
+
+  it('correctly renders which browsers support FID', async function () {
+    const newRouter = {
+      ...router,
+      location: {
+        ...router.location,
+        query: {
+          vitalName: 'measurements.fid',
+        },
+      },
+    };
+
+    mountWithTheme(<TestComponent router={newRouter} />, {
+      context: routerContext,
+    });
+
+    testSupportedBrowserRendering(WebVital.FID);
+  });
 });