123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231 |
- import React, {useEffect} from 'react';
- import styled from '@emotion/styled';
- import {Location} from 'history';
- import {
- INDUSTRY_STANDARDS,
- MIN_VITAL_COUNT_FOR_DISPLAY,
- SENTRY_CUSTOMERS,
- } from 'sentry/components/performance/vitalsAlert/constants';
- import QuestionTooltip from 'sentry/components/questionTooltip';
- import Tag from 'sentry/components/tag';
- import {t, tct} from 'sentry/locale';
- import space from 'sentry/styles/space';
- import {Organization} from 'sentry/types';
- import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
- import EventView from 'sentry/utils/discover/eventView';
- import {WebVital} from 'sentry/utils/fields';
- import VitalsCardDiscoverQuery from 'sentry/utils/performance/vitals/vitalsCardsDiscoverQuery';
- import {webVitalMeh, webVitalPoor} from 'sentry/views/performance/vitalDetail/utils';
- type Score = 'poor' | 'meh' | 'good';
- type ViewProps = Pick<
- EventView,
- 'environment' | 'project' | 'start' | 'end' | 'statsPeriod'
- >;
- type Props = ViewProps & {
- location: Location;
- organization: Organization;
- vital: WebVital | WebVital[];
- };
- const SUPPORTED_VITALS = ['measurements.fcp', 'measurements.lcp'];
- function getScore(vital: WebVital, value: number): Score {
- const poorScore = webVitalPoor[vital];
- const mehScore = webVitalMeh[vital];
- if (value > poorScore) {
- return 'poor';
- }
- if (value > mehScore) {
- return 'meh';
- }
- return 'good';
- }
- function getIndicatorString(score: Score) {
- switch (score) {
- case 'poor':
- return t('Poor');
- case 'meh':
- return t('Meh');
- default:
- return t('Good');
- }
- }
- function getTagLevel(score: Score) {
- switch (score) {
- case 'poor':
- return 'error';
- case 'meh':
- return 'warning';
- default:
- return 'success';
- }
- }
- function MetricsCard({
- title,
- vital,
- value,
- tooltip,
- }: {
- title: string;
- tooltip: string;
- value: number;
- vital: WebVital;
- }) {
- // round to 2 decimals if <10s, otherwise use just 1 decimal
- const score = getScore(vital, value);
- const numDecimals = value >= 10_000 ? 1 : 2;
- const timeInSeconds = value / 1000.0;
- return (
- <MetricsCardWrapper>
- <MetricsTitle>
- {title} (p75) <StyledQuestionTooltip title={tooltip} size="xs" />
- </MetricsTitle>
- <ScoreWrapper>
- <ScoreContent>{timeInSeconds.toFixed(numDecimals)}s</ScoreContent>
- <TagWrapper>
- <StyledTag type={getTagLevel(score)}>{getIndicatorString(score)}</StyledTag>
- </TagWrapper>
- </ScoreWrapper>
- </MetricsCardWrapper>
- );
- }
- function ContentWrapper({
- organization,
- vital,
- children,
- count,
- p75,
- }: {
- children: React.ReactNode;
- count: number;
- organization: Organization;
- p75: number;
- vital: WebVital;
- }) {
- useEffect(() => {
- trackAdvancedAnalyticsEvent('performance_views.vital_detail.comparison_viewed', {
- organization,
- vital,
- count,
- p75,
- });
- });
- return <Container>{children}</Container>;
- }
- function VitalsComparison(props: Props) {
- const {location, vital: _vital, organization} = props;
- const vitals = Array.isArray(_vital) ? _vital : [_vital];
- const vital = vitals[0];
- if (!SUPPORTED_VITALS.includes(vital)) {
- return null;
- }
- return (
- <VitalsCardDiscoverQuery location={location} vitals={vitals}>
- {({isLoading, vitalsData}) => {
- if (isLoading || !vitalsData) {
- return null;
- }
- const data = vitalsData[vital];
- if (!data || !data.p75) {
- return null;
- }
- const {p75} = data;
- const lookupName = vital === 'measurements.fcp' ? 'FCP' : 'LCP';
- const sentryStandard = SENTRY_CUSTOMERS[lookupName];
- const industryStandard = INDUSTRY_STANDARDS[lookupName];
- const count = vitalsData[vital].total;
- // only show it if we hit the min number
- if (count < MIN_VITAL_COUNT_FOR_DISPLAY) {
- return null;
- }
- return (
- <ContentWrapper {...{organization, vital, count, p75}}>
- <MetricsCard
- title={t('Selected Projects')}
- vital={vital}
- value={p75}
- tooltip={tct(
- "25% of your project's transactions have an [lookupName] greater than this number. Good, Bad, Meh segmentation is based on Google industry standards.",
- {lookupName}
- )}
- />
- <MetricsCard
- title={t('Sentry Peers')}
- vital={vital}
- value={sentryStandard}
- tooltip={tct(
- '20% of Sentry customers have a p75 [lookupName] lower than this.',
- {lookupName}
- )}
- />
- <MetricsCard
- title={t('Industry Standard')}
- vital={vital}
- value={industryStandard}
- tooltip={tct(
- "Calculated as a Good [lookupName] based on Google's industry standards.",
- {lookupName}
- )}
- />
- </ContentWrapper>
- );
- }}
- </VitalsCardDiscoverQuery>
- );
- }
- export default VitalsComparison;
- const Container = styled('div')`
- display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- gap: ${space(2)};
- `;
- const ScoreContent = styled('h6')`
- margin: auto;
- `;
- const ScoreWrapper = styled('div')`
- display: flex;
- align-items: center;
- `;
- const MetricsCardWrapper = styled('div')`
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- border: 1px ${p => p.theme.gray200};
- border-radius: 4px;
- border-style: solid;
- align-items: center;
- height: 57px;
- padding: ${space(2)};
- margin-bottom: ${space(2)};
- `;
- const StyledTag = styled(Tag)`
- margin-left: ${space(1)};
- `;
- const MetricsTitle = styled('span')`
- font-size: 14px;
- `;
- const TagWrapper = styled('span')`
- margin: auto;
- `;
- const StyledQuestionTooltip = styled(QuestionTooltip)`
- position: relative;
- top: 1px;
- `;
|