From 7978123593814cf2405283c1b79d1ba621ffe436 Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Wed, 15 Jan 2025 01:06:23 -0300 Subject: [PATCH] feat: Add CoreWebVitalsContent This is the inner part of the Core Web Vitals view, the UI is now mostly complete, we're only missing the split by paths to get UI parity with Vercel (we'll miss some of the 0-100 analysis but that's harder to do, we'll release that eventually) --- .../nodes/CoreWebVitals/CoreWebVitals.tsx | 174 ++++++++++++++++-- 1 file changed, 159 insertions(+), 15 deletions(-) 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 */}