[Uptime] Fix/85236 user experience display low values (#86026)

* add hasVitals prop to CoreVitalItem

* pass hasVitals prop to CoreVitalsItem based on coreVitalPages

* adjust criteria for displaying no core vital item data

* add stories for CoreVitalItem edge cases

* remove comment from core web vitals index page

* update test comment in CoreVitalItem

* adjust APM get_web_core_vitals endpoint to return a number for cls value, and adjust corresponding observability components

* remove hasVitals from CoreVitalItem props and adjust storybook stories

* add comment to EuiStat aria-label in CoreVitalItem

* adjust CoreVitalItem tests

* adjust APM KeyUXMetrics test

* adjust APM get_web_core_vitals endpoint to return null for cls when cls is undefined

* adjust unit and integration tests that rely on apm get_web_core_vitals

* add comment in get_web_core_vitals

* update CLS value in Observability core_web_vitals index

* add withKibanaIntl to CoreVitalItem test to wrap in Intl Provider and KibanaReact provider

* update CoreVitalItem test to use testing-library/react test_helper

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dominique Clarke 2020-12-23 14:25:34 -05:00 committed by GitHub
parent f7961998d9
commit cf6afe04ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 104 additions and 20 deletions

View file

@ -23,7 +23,7 @@ describe('KeyUXMetrics', () => {
<KeyUXMetrics
loading={false}
data={{
cls: '0.01',
cls: 0.01,
fid: 6,
lcp: 1701.1142857142856,
tbt: 270.915,

View file

@ -132,7 +132,9 @@ export async function getWebCoreVitals({
return {
coreVitalPages: coreVitalPages?.doc_count ?? 0,
cls: cls?.values[pkey]?.toFixed(3) || null,
/* Because cls is required in the type UXMetrics, and defined as number | null,
* we need to default to null in the case where cls is undefined in order to satisfy the UXMetrics type */
cls: cls?.values[pkey] ?? null,
fid: fid?.values[pkey],
lcp: lcp?.values[pkey],
tbt: tbt?.values[pkey] ?? 0,

View file

@ -67,7 +67,7 @@ describe('UXSection', () => {
expect(getByText('Largest contentful paint')).toBeInTheDocument();
expect(getByText('1.94 s')).toBeInTheDocument();
expect(getByText('14 ms')).toBeInTheDocument();
expect(getByText('0.01')).toBeInTheDocument();
expect(getByText('0.010')).toBeInTheDocument();
// LCP Rank Values
expect(getByText('Good (65%)')).toBeInTheDocument();

View file

@ -9,7 +9,7 @@ import { UxFetchDataResponse } from '../../../../../typings';
export const response: UxFetchDataResponse = {
appLink: '/app/ux',
coreWebVitals: {
cls: '0.01',
cls: 0.01,
fid: 13.5,
lcp: 1942.6666666666667,
tbt: 281.55833333333334,

View file

@ -33,13 +33,26 @@ export default {
],
};
export function Basic() {
export function NoDataAvailable() {
return (
<CoreVitalItem
thresholds={{ good: '0.1', bad: '0.25' }}
title={LCP_LABEL}
value={null}
loading={false}
helpLabel={LCP_HELP_LABEL}
/>
);
}
export function OneHundredPercentGood() {
return (
<CoreVitalItem
thresholds={{ good: '0.1', bad: '0.25' }}
title={LCP_LABEL}
value={'0.00s'}
loading={false}
ranks={[100, 0, 0]}
helpLabel={LCP_HELP_LABEL}
/>
);

View file

@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { render } from '../../../utils/test_helper';
import { CoreVitalItem } from './core_vital_item';
import { NO_DATA } from './translations';
describe('CoreVitalItem', () => {
const value = '0.005';
const title = 'Cumulative Layout Shift';
const thresholds = { bad: '0.25', good: '0.1' };
const loading = false;
const helpLabel = 'sample help label';
it('renders if value is truthy', () => {
const { getByText } = render(
<CoreVitalItem
title={title}
value={value}
ranks={[85, 10, 5]}
loading={loading}
thresholds={thresholds}
helpLabel={helpLabel}
/>
);
expect(getByText(title)).toBeInTheDocument();
expect(getByText(value)).toBeInTheDocument();
expect(getByText('Good (85%)')).toBeInTheDocument();
expect(getByText('Needs improvement (10%)')).toBeInTheDocument();
expect(getByText('Poor (5%)')).toBeInTheDocument();
});
it('renders loading state when loading is truthy', () => {
const { queryByText, getByText } = render(
<CoreVitalItem
title={title}
value={value}
ranks={[85, 10, 5]}
loading={true}
thresholds={thresholds}
helpLabel={helpLabel}
/>
);
expect(queryByText(value)).not.toBeInTheDocument();
expect(getByText('--')).toBeInTheDocument();
});
it('renders no data UI if value is falsey and loading is falsey', () => {
const { getByText } = render(
<CoreVitalItem
title={title}
value={null}
ranks={[85, 10, 5]}
loading={loading}
thresholds={thresholds}
helpLabel={helpLabel}
/>
);
expect(getByText(NO_DATA)).toBeInTheDocument();
});
});

View file

@ -88,12 +88,14 @@ export function CoreVitalItem({
const biggestValIndex = ranks.indexOf(Math.max(...ranks));
if ((value === null || value !== undefined) && ranks[0] === 100 && !loading) {
if (!value && !loading) {
return <EuiCard title={title} isDisabled={true} description={NO_DATA} />;
}
return (
<>
<EuiStat
aria-label={`${title} ${value}`} // aria-label is required when passing a component, instead of a string, as the description
titleSize="s"
title={value ?? ''}
description={

View file

@ -18,7 +18,7 @@ import { WebCoreVitalsTitle } from './web_core_vitals_title';
import { ServiceName } from './service_name';
export interface UXMetrics {
cls: string | null;
cls: number | null;
fid?: number | null;
lcp?: number | null;
tbt: number;
@ -38,6 +38,13 @@ export function formatToSec(value?: number | string, fromUnit = 'MicroSec'): str
return (valueInMs / 1000).toFixed(2) + ' s';
}
function formatToMilliseconds(value?: number | null) {
if (typeof value === 'undefined' || value === null) {
return null;
}
return formatToSec(value, 'ms');
}
const CoreVitalsThresholds = {
LCP: { good: '2.5s', bad: '4.0s' },
FID: { good: '100ms', bad: '300ms' },
@ -53,13 +60,6 @@ interface Props {
displayTrafficMetric?: boolean;
}
function formatValue(value?: number | null) {
if (typeof value === 'undefined' || value === null) {
return null;
}
return formatToSec(value, 'ms');
}
export function CoreVitals({
data,
loading,
@ -85,7 +85,7 @@ export function CoreVitals({
<EuiFlexItem style={{ flexBasis: 380 }}>
<CoreVitalItem
title={LCP_LABEL}
value={formatValue(lcp)}
value={formatToMilliseconds(lcp)}
ranks={lcpRanks}
loading={loading}
thresholds={CoreVitalsThresholds.LCP}
@ -95,7 +95,7 @@ export function CoreVitals({
<EuiFlexItem style={{ flexBasis: 380 }}>
<CoreVitalItem
title={FID_LABEL}
value={formatValue(fid)}
value={formatToMilliseconds(fid)}
ranks={fidRanks}
loading={loading}
thresholds={CoreVitalsThresholds.FID}
@ -105,7 +105,7 @@ export function CoreVitals({
<EuiFlexItem style={{ flexBasis: 380 }}>
<CoreVitalItem
title={CLS_LABEL}
value={cls ?? null}
value={cls?.toFixed(3) ?? null}
ranks={clsRanks}
loading={loading}
thresholds={CoreVitalsThresholds.CLS}

View file

@ -270,7 +270,7 @@ describe('registerDataHandler', () => {
title: 'User Experience',
appLink: '/ux',
coreWebVitals: {
cls: '0.01',
cls: 0.01,
fid: 5,
lcp: 1464.3333333333333,
tbt: 232.92166666666665,
@ -298,7 +298,7 @@ describe('registerDataHandler', () => {
title: 'User Experience',
appLink: '/ux',
coreWebVitals: {
cls: '0.01',
cls: 0.01,
fid: 5,
lcp: 1464.3333333333333,
tbt: 232.92166666666665,

View file

@ -49,7 +49,7 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext)
expectSnapshot(response.body).toMatchInline(`
Object {
"cls": "0.000",
"cls": 0,
"clsRanks": Array [
100,
0,