123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- import {Fragment} from 'react';
- import styled from '@emotion/styled';
- import {SectionHeading} from 'sentry/components/charts/styles';
- import {Panel} from 'sentry/components/panels';
- import {Tooltip} from 'sentry/components/tooltip';
- import {IconFire, IconWarning} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import {Event} from 'sentry/types/event';
- import {defined} from 'sentry/utils';
- import {formattedValue} from 'sentry/utils/measurements/index';
- import {
- MOBILE_VITAL_DETAILS,
- WEB_VITAL_DETAILS,
- } from 'sentry/utils/performance/vitals/constants';
- import {Vital} from 'sentry/utils/performance/vitals/types';
- import {IconSize} from 'sentry/utils/theme';
- function isOutdatedSdk(event: Event): boolean {
- if (!event.sdk?.version) {
- return false;
- }
- const sdkVersion = event.sdk.version;
- return (
- sdkVersion.startsWith('5.26.') ||
- sdkVersion.startsWith('5.27.0') ||
- sdkVersion.startsWith('5.27.1') ||
- sdkVersion.startsWith('5.27.2')
- );
- }
- type Props = {
- event: Event;
- };
- export default function EventVitals({event}: Props) {
- return (
- <Fragment>
- <WebVitals event={event} />
- <MobileVitals event={event} />
- </Fragment>
- );
- }
- function WebVitals({event}: Props) {
- const measurementNames = Object.keys(event.measurements ?? {})
- .filter(name => Boolean(WEB_VITAL_DETAILS[`measurements.${name}`]))
- .sort();
- if (measurementNames.length === 0) {
- return null;
- }
- return (
- <Container>
- <SectionHeading>
- {t('Web Vitals')}
- {isOutdatedSdk(event) && (
- <WarningIconContainer data-test-id="outdated-sdk-warning" size="sm">
- <Tooltip
- title={t(
- 'These vitals were collected using an outdated SDK version and may not be accurate. To ensure accurate web vitals in new transaction events, please update your SDK to the latest version.'
- )}
- position="top"
- containerDisplayMode="inline-block"
- >
- <IconWarning size="sm" />
- </Tooltip>
- </WarningIconContainer>
- )}
- </SectionHeading>
- <Measurements>
- {measurementNames.map(name => {
- // Measurements are referred to by their full name `measurements.<name>`
- // here but are stored using their abbreviated name `<name>`. Make sure
- // to convert it appropriately.
- const measurement = `measurements.${name}`;
- const vital = WEB_VITAL_DETAILS[measurement];
- return <EventVital key={name} event={event} name={name} vital={vital} />;
- })}
- </Measurements>
- </Container>
- );
- }
- function MobileVitals({event}: Props) {
- const measurementNames = Object.keys(event.measurements ?? {})
- .filter(name => Boolean(MOBILE_VITAL_DETAILS[`measurements.${name}`]))
- .sort();
- if (measurementNames.length === 0) {
- return null;
- }
- return (
- <Container>
- <SectionHeading>{t('Mobile Vitals')}</SectionHeading>
- <Measurements>
- {measurementNames.map(name => {
- // Measurements are referred to by their full name `measurements.<name>`
- // here but are stored using their abbreviated name `<name>`. Make sure
- // to convert it appropriately.
- const measurement = `measurements.${name}`;
- const vital = MOBILE_VITAL_DETAILS[measurement];
- return <EventVital key={name} event={event} name={name} vital={vital} />;
- })}
- </Measurements>
- </Container>
- );
- }
- type EventVitalProps = Props & {
- name: string;
- vital?: Vital;
- };
- function EventVital({event, name, vital}: EventVitalProps) {
- const value = event.measurements?.[name].value ?? null;
- if (value === null || !vital) {
- return null;
- }
- const failedThreshold = defined(vital.poorThreshold) && value >= vital.poorThreshold;
- const currentValue = formattedValue(vital, value);
- const thresholdValue = formattedValue(vital, vital?.poorThreshold ?? 0);
- return (
- <EventVitalContainer>
- <StyledPanel failedThreshold={failedThreshold}>
- <Name>{vital.name ?? name}</Name>
- <ValueRow>
- {failedThreshold ? (
- <FireIconContainer data-test-id="threshold-failed-warning" size="sm">
- <Tooltip
- title={t('Fails threshold at %s.', thresholdValue)}
- position="top"
- containerDisplayMode="inline-block"
- >
- <IconFire size="sm" />
- </Tooltip>
- </FireIconContainer>
- ) : null}
- <Value failedThreshold={failedThreshold}>{currentValue}</Value>
- </ValueRow>
- </StyledPanel>
- </EventVitalContainer>
- );
- }
- const Measurements = styled('div')`
- display: grid;
- grid-column-gap: ${space(1)};
- `;
- const Container = styled('div')`
- font-size: ${p => p.theme.fontSizeMedium};
- margin-bottom: ${space(4)};
- `;
- const StyledPanel = styled(Panel)<{failedThreshold: boolean}>`
- padding: ${space(1)} ${space(1.5)};
- margin-bottom: ${space(1)};
- ${p => p.failedThreshold && `border: 1px solid ${p.theme.red300};`}
- `;
- const Name = styled('div')``;
- const ValueRow = styled('div')`
- display: flex;
- align-items: center;
- `;
- const WarningIconContainer = styled('span')<{size: IconSize | string}>`
- display: inline-block;
- height: ${p => p.theme.iconSizes[p.size] ?? p.size};
- line-height: ${p => p.theme.iconSizes[p.size] ?? p.size};
- margin-left: ${space(0.5)};
- color: ${p => p.theme.errorText};
- `;
- const FireIconContainer = styled('span')<{size: IconSize | string}>`
- display: inline-block;
- height: ${p => p.theme.iconSizes[p.size] ?? p.size};
- line-height: ${p => p.theme.iconSizes[p.size] ?? p.size};
- margin-right: ${space(0.5)};
- color: ${p => p.theme.errorText};
- `;
- const Value = styled('span')<{failedThreshold: boolean}>`
- font-size: ${p => p.theme.fontSizeExtraLarge};
- ${p => p.failedThreshold && `color: ${p.theme.errorText};`}
- `;
- export const EventVitalContainer = styled('div')``;
|