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 (
{title} (p75)
{timeInSeconds.toFixed(numDecimals)}s
{getIndicatorString(score)}
);
}
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 {children};
}
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 (
{({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 (
);
}}
);
}
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;
`;