Skip to content

Commit

Permalink
[RUM Dashboard] User experience metrics (#77384)
Browse files Browse the repository at this point in the history
Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
shahzad31 and elasticmachine authored Sep 16, 2020
1 parent 6c5258a commit 10b192b
Show file tree
Hide file tree
Showing 21 changed files with 8,374 additions and 391 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,22 @@
*/
import * as React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';

import { useFetcher } from '../../../../hooks/useFetcher';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { CLS_LABEL, FID_LABEL, LCP_LABEL } from './translations';
import { CoreVitalItem } from './CoreVitalItem';
import { UXMetrics } from '../UXMetrics';

const CoreVitalsThresholds = {
LCP: { good: '2.5s', bad: '4.0s' },
FID: { good: '100ms', bad: '300ms' },
CLS: { good: '0.1', bad: '0.25' },
};

export function CoreVitals() {
const { urlParams, uiFilters } = useUrlParams();

const { start, end } = urlParams;

const { data, status } = useFetcher(
(callApmApi) => {
const { serviceName } = uiFilters;
if (start && end && serviceName) {
return callApmApi({
pathname: '/api/apm/rum-client/web-core-vitals',
params: {
query: { start, end, uiFilters: JSON.stringify(uiFilters) },
},
});
}
return Promise.resolve(null);
},
[start, end, uiFilters]
);
interface Props {
data?: UXMetrics | null;
loading: boolean;
}

export function CoreVitals({ data, loading }: Props) {
const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks } = data || {};

return (
Expand All @@ -47,7 +30,7 @@ export function CoreVitals() {
title={LCP_LABEL}
value={lcp ? lcp + 's' : '0'}
ranks={lcpRanks}
loading={status !== 'success'}
loading={loading}
thresholds={CoreVitalsThresholds.LCP}
/>
</EuiFlexItem>
Expand All @@ -56,7 +39,7 @@ export function CoreVitals() {
title={FID_LABEL}
value={fid ? fid + 's' : '0'}
ranks={fidRanks}
loading={status !== 'success'}
loading={loading}
thresholds={CoreVitalsThresholds.FID}
/>
</EuiFlexItem>
Expand All @@ -65,7 +48,7 @@ export function CoreVitals() {
title={CLS_LABEL}
value={cls ?? '0'}
ranks={clsRanks}
loading={status !== 'success'}
loading={loading}
thresholds={CoreVitalsThresholds.CLS}
/>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ export const TBT_LABEL = i18n.translate('xpack.apm.rum.coreVitals.tbt', {
defaultMessage: 'Total blocking time',
});

export const NO_OF_LONG_TASK = i18n.translate(
'xpack.apm.rum.uxMetrics.noOfLongTasks',
{
defaultMessage: 'No. of long tasks',
}
);

export const LONGEST_LONG_TASK = i18n.translate(
'xpack.apm.rum.uxMetrics.longestLongTasks',
{
defaultMessage: 'Longest long task duration',
}
);

export const SUM_LONG_TASKS = i18n.translate(
'xpack.apm.rum.uxMetrics.sumLongTasks',
{
defaultMessage: 'Total long tasks duration',
}
);

export const POOR_LABEL = i18n.translate('xpack.apm.rum.coreVitals.poor', {
defaultMessage: 'a poor',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { PageViewsTrend } from './PageViewsTrend';
import { PageLoadDistribution } from './PageLoadDistribution';
import { I18LABELS } from './translations';
import { VisitorBreakdown } from './VisitorBreakdown';
import { CoreVitals } from './CoreVitals';
import { UXMetrics } from './UXMetrics';
import { VisitorBreakdownMap } from './VisitorBreakdownMap';

export function RumDashboard() {
Expand All @@ -37,17 +37,7 @@ export function RumDashboard() {
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={1} data-cy={`client-metrics`}>
<EuiTitle size="xs">
<h3>{I18LABELS.coreWebVitals}</h3>
</EuiTitle>
<EuiSpacer size="s" />
<CoreVitals />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<UXMetrics />
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s" wrap>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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 { EuiFlexItem, EuiStat, EuiFlexGroup } from '@elastic/eui';
import { UXMetrics } from './index';
import {
FCP_LABEL,
LONGEST_LONG_TASK,
NO_OF_LONG_TASK,
SUM_LONG_TASKS,
TBT_LABEL,
} from '../CoreVitals/translations';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { useFetcher } from '../../../../hooks/useFetcher';

export function formatToSec(
value?: number | string,
fromUnit = 'MicroSec'
): string {
const valueInMs = Number(value ?? 0) / (fromUnit === 'MicroSec' ? 1000 : 1);

if (valueInMs < 1000) {
return valueInMs + ' ms';
}
return (valueInMs / 1000).toFixed(2) + ' s';
}
const STAT_STYLE = { width: '240px' };

interface Props {
data?: UXMetrics | null;
loading: boolean;
}

export function KeyUXMetrics({ data, loading }: Props) {
const { urlParams, uiFilters } = useUrlParams();

const { start, end, serviceName } = urlParams;

const { data: longTaskData, status } = useFetcher(
(callApmApi) => {
if (start && end && serviceName) {
return callApmApi({
pathname: '/api/apm/rum-client/long-task-metrics',
params: {
query: { start, end, uiFilters: JSON.stringify(uiFilters) },
},
});
}
return Promise.resolve(null);
},
[start, end, serviceName, uiFilters]
);

// Note: FCP value is in ms unit
return (
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow={false} style={STAT_STYLE}>
<EuiStat
titleSize="s"
title={formatToSec(data?.fcp, 'ms')}
description={FCP_LABEL}
isLoading={loading}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={STAT_STYLE}>
<EuiStat
titleSize="s"
title={formatToSec(data?.tbt)}
description={TBT_LABEL}
isLoading={loading}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={STAT_STYLE}>
<EuiStat
titleSize="s"
title={longTaskData?.noOfLongTasks ?? 0}
description={NO_OF_LONG_TASK}
isLoading={status !== 'success'}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={STAT_STYLE}>
<EuiStat
titleSize="s"
title={formatToSec(longTaskData?.longestLongTask)}
description={LONGEST_LONG_TASK}
isLoading={status !== 'success'}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={STAT_STYLE}>
<EuiStat
titleSize="s"
title={formatToSec(longTaskData?.sumOfLongTasks)}
description={SUM_LONG_TASKS}
isLoading={status !== 'success'}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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 { formatToSec } from '../KeyUXMetrics';

describe('FormatToSec', () => {
test('it returns the expected value', () => {
expect(formatToSec(3413000)).toStrictEqual('3.41 s');
expect(formatToSec(15548000)).toStrictEqual('15.55 s');
expect(formatToSec(1147.5, 'ms')).toStrictEqual('1.15 s');
expect(formatToSec(114, 'ms')).toStrictEqual('114 ms');
expect(formatToSec(undefined, 'ms')).toStrictEqual('0 ms');
expect(formatToSec(undefined)).toStrictEqual('0 ms');
expect(formatToSec('1123232')).toStrictEqual('1.12 s');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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 {
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiPanel,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { I18LABELS } from '../translations';
import { CoreVitals } from '../CoreVitals';
import { KeyUXMetrics } from './KeyUXMetrics';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { useFetcher } from '../../../../hooks/useFetcher';

export interface UXMetrics {
cls: string;
fid: string;
lcp: string;
tbt: string;
fcp: number;
lcpRanks: number[];
fidRanks: number[];
clsRanks: number[];
}

export function UXMetrics() {
const { urlParams, uiFilters } = useUrlParams();

const { start, end } = urlParams;

const { data, status } = useFetcher(
(callApmApi) => {
const { serviceName } = uiFilters;
if (start && end && serviceName) {
return callApmApi({
pathname: '/api/apm/rum-client/web-core-vitals',
params: {
query: { start, end, uiFilters: JSON.stringify(uiFilters) },
},
});
}
return Promise.resolve(null);
},
[start, end, uiFilters]
);

return (
<EuiPanel>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={1} data-cy={`client-metrics`}>
<EuiTitle size="s">
<h2>{I18LABELS.userExperienceMetrics}</h2>
</EuiTitle>
<EuiSpacer size="s" />
<KeyUXMetrics data={data} loading={status !== 'success'} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule />

<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={1} data-cy={`client-metrics`}>
<EuiTitle size="xs">
<h3>{I18LABELS.coreWebVitals}</h3>
</EuiTitle>
<EuiSpacer size="s" />
<CoreVitals data={data} loading={status !== 'success'} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export const I18LABELS = {
defaultMessage: 'Operating system',
}
),
userExperienceMetrics: i18n.translate('xpack.apm.rum.userExperienceMetrics', {
defaultMessage: 'User experience metrics',
}),
avgPageLoadDuration: i18n.translate(
'xpack.apm.rum.visitorBreakdownMap.avgPageLoadDuration',
{
Expand Down
Loading

0 comments on commit 10b192b

Please sign in to comment.