diff --git a/frontend/src/queries/nodes/CoreWebVitals/CoreWebVitals.tsx b/frontend/src/queries/nodes/CoreWebVitals/CoreWebVitals.tsx index c6dd9c40e2128..05e6f6ce0d53e 100644 --- a/frontend/src/queries/nodes/CoreWebVitals/CoreWebVitals.tsx +++ b/frontend/src/queries/nodes/CoreWebVitals/CoreWebVitals.tsx @@ -1,8 +1,10 @@ import './CoreWebVitals.scss' +import { IconCheckCircle, IconInfo, IconWarning } from '@posthog/icons' import { LemonSkeleton, Tooltip } from '@posthog/lemon-ui' import { clsx } from 'clsx' import { useActions, useValues } from 'kea' +import { IconExclamation } from 'lib/lemon-ui/icons' import { useMemo, useState } from 'react' import { CORE_WEB_VITALS_THRESHOLDS, @@ -23,6 +25,66 @@ import { QueryContext } from '~/queries/types' import { dataNodeLogic } from '../DataNode/dataNodeLogic' +type MetricBand = 'good' | 'improvements' | 'poor' + +const LONG_METRIC_NAME: Record = { + INP: 'Interaction to Next Paint', + LCP: 'Largest Contentful Paint', + FCP: 'First Contentful Paint', + CLS: 'Cumulative Layout Shift', +} + +const METRIC_DESCRIPTION: Record = { + INP: 'Measures the time it takes for the user to interact with the page and for the page to respond to the interaction. Lower is better.', + LCP: 'Measures how long it takes for the main content of a page to appear on screen. Lower is better.', + FCP: 'Measures how long it takes for the initial text, non-white background, and non-white text to appear on screen. Lower is better.', + CLS: 'Measures how much the layout of a page shifts around as content loads. Lower is better.', +} + +const PERCENTILE_NAME: Record = { + p75: '75%', + p90: '90%', + p99: '99%', +} + +const ICON_PER_BAND: Record = { + good: IconCheckCircle, + improvements: IconWarning, + poor: IconExclamation, +} + +const GRADE_PER_BAND: Record = { + good: 'Great', + improvements: 'Needs Improvement', + poor: 'Poor', +} + +const POSITIONING_PER_BAND: Record = { + good: 'Below', + improvements: 'Between', + poor: 'Above', +} + +const VALUES_PER_BAND: Record number[]> = { + good: (threshold) => [threshold.good], + improvements: (threshold) => [threshold.good, threshold.poor], + poor: (threshold) => [threshold.poor], +} + +const QUANTIFIER_PER_BAND: Record string> = { + good: (coreWebVitalsPercentile) => `More than ${PERCENTILE_NAME[coreWebVitalsPercentile]} of visits had`, + improvements: (coreWebVitalsPercentile) => + `Some of the ${PERCENTILE_NAME[coreWebVitalsPercentile]} most performatic visits had`, + poor: (coreWebVitalsPercentile) => + `Some of the ${PERCENTILE_NAME[coreWebVitalsPercentile]} most performatic visits had`, +} + +const EXPERIENCE_PER_BAND: Record = { + good: 'a great experience', + improvements: 'an experience that needs improvement', + poor: 'a poor experience', +} + const getMetric = ( results: CoreWebVitalsItem[] | undefined, metric: CoreWebVitalsMetric, @@ -34,6 +96,18 @@ const getMetric = ( ?.data.slice(-1)[0] } +const getMetricBand = (value: number, threshold: CoreWebVitalsThreshold): MetricBand => { + if (value <= threshold.good) { + return 'good' + } + + if (value <= threshold.poor) { + return 'improvements' + } + + return 'poor' +} + let uniqueNode = 0 export function CoreWebVitals(props: { query: CoreWebVitalsQuery @@ -77,7 +151,7 @@ export function CoreWebVitals(props: {
setCoreWebVitalsTab('INP')} @@ -85,7 +159,7 @@ export function CoreWebVitals(props: { /> setCoreWebVitalsTab('LCP')} @@ -93,7 +167,7 @@ export function CoreWebVitals(props: { /> setCoreWebVitalsTab('FCP')} @@ -101,22 +175,92 @@ export function CoreWebVitals(props: { /> setCoreWebVitalsTab('CLS')} />
-
- Actual content - +
+ +
+ +
+
+ ) +} + +const CoreWebVitalsContent = ({ + coreWebVitalsQueryResponse, +}: { + coreWebVitalsQueryResponse?: CoreWebVitalsQueryResponse +}): JSX.Element => { + const { coreWebVitalsTab, coreWebVitalsPercentile } = useValues(webAnalyticsLogic) + + const value = useMemo( + () => getMetric(coreWebVitalsQueryResponse?.results, coreWebVitalsTab, coreWebVitalsPercentile), + [coreWebVitalsQueryResponse, coreWebVitalsPercentile, coreWebVitalsTab] + ) + + if (value === undefined) { + return ( +
+ +
+ ) + } + + const withMilliseconds = (values: number[]): string => + coreWebVitalsTab === 'CLS' ? values.join(' and ') : values.map((value) => `${value}ms`).join(' and ') + + const threshold = CORE_WEB_VITALS_THRESHOLDS[coreWebVitalsTab] + const color = getThresholdColor(value, threshold) + const band = getMetricBand(value, threshold) + + const grade = GRADE_PER_BAND[band] + + const Icon = ICON_PER_BAND[band] + const positioning = POSITIONING_PER_BAND[band] + const values = withMilliseconds(VALUES_PER_BAND[band](threshold)) + + const quantifier = QUANTIFIER_PER_BAND[band](coreWebVitalsPercentile) + const experience = EXPERIENCE_PER_BAND[band] + + return ( +
+ + {LONG_METRIC_NAME[coreWebVitalsTab]} + + +
+ + Great: Below {threshold.good}ms
+ Needs Improvement: Between {threshold.good}ms and {threshold.poor}ms
+ Poor: Above {threshold.poor}ms +
+ } + > + {grade} + + + + + + {positioning} {values} + +
+ +
+ {quantifier} {experience} +
+ +
- {coreWebVitalsTab === 'INP' &&
INP
} - {coreWebVitalsTab === 'LCP' &&
LCP
} - {coreWebVitalsTab === 'CLS' &&
CLS
} - {coreWebVitalsTab === 'FCP' &&
FCP
} + {METRIC_DESCRIPTION[coreWebVitalsTab]} ) } @@ -168,7 +312,7 @@ function CoreWebVitalsTab({ label: string metric: CoreWebVitalsMetric isActive: boolean - setTab: () => void + setTab?: () => void inSeconds?: boolean }): JSX.Element { // TODO: Go back to using an actual value @@ -226,21 +370,21 @@ export function ProgressBar({ value, threshold }: ProgressBarProps): JSX.Element
{/* Green segment up to "good" threshold */}
{/* Yellow segment up to "poor" threshold */}
{/* Red segment after "poor" threshold */}