[APM] Display comparison for mobile stats (#149097)

## Summary 

related to https://github.com/elastic/kibana/issues/146854
1. Show the comparison for the mobile stats
2. Display a badge "comparison not supported" (when the comparison is
enabled )for the components that don't support comparison
3. display "coming soon" text for the metrics that are not available yet


Addressing feedback
- Replace the badge with a tooltip with an icon
- Always display the previous state for metrics when loading and add the
spinner
- Update Crash rate to Crash rate (Crash per minute)
- Remove fallback to transaction events badge



## Before
<img width="1420" alt="image"
src="https://user-images.githubusercontent.com/3369346/213138845-3eab0bf5-a24e-4ec0-87fb-d8eacc029a2f.png">

## After 


![Jan-25-2023
09-52-53](https://user-images.githubusercontent.com/3369346/214520021-6ff04d13-250a-47bd-b983-66c6f35cfb46.gif)

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Katerina Patticha 2023-01-25 13:56:53 +01:00 committed by GitHub
parent 43247bdc0f
commit 026d347305
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 152 additions and 74 deletions

View file

@ -356,7 +356,9 @@ const scenario: Scenario<ApmFields> = async ({ scenarioOpts, logger }) => {
.span({
spanName: 'onCreate',
spanType: 'app',
spanSubtype: 'internal',
spanSubtype: 'external',
'service.target.type': 'http',
'span.destination.service.resource': 'external',
})
.duration(50)
.success()

View file

@ -16,6 +16,7 @@ import {
EuiSpacer,
EuiTitle,
EuiCallOut,
EuiIconTip,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
@ -38,7 +39,6 @@ import { MostUsedChart } from './most_used_chart';
import { LatencyMap } from './latency_map';
import { FailedTransactionRateChart } from '../../../shared/charts/failed_transaction_rate_chart';
import { ServiceOverviewDependenciesTable } from '../../service_overview/service_overview_dependencies_table';
import { AggregatedTransactionsBadge } from '../../../shared/aggregated_transactions_badge';
import { LatencyChart } from '../../../shared/charts/latency_chart';
import { useFiltersForEmbeddableCharts } from '../../../../hooks/use_filters_for_embeddable_charts';
import { getKueryWithMobileFilters } from '../../../../../common/utils/get_kuery_with_mobile_filters';
@ -50,7 +50,7 @@ import { MobileStats } from './stats';
export const chartHeight = 288;
export function MobileServiceOverview() {
const { serviceName, fallbackToTransactions } = useApmServiceContext();
const { serviceName } = useApmServiceContext();
const router = useApmRouter();
const embeddableFilters = useFiltersForEmbeddableCharts();
@ -65,6 +65,7 @@ export function MobileServiceOverview() {
osVersion,
appVersion,
netConnectionType,
comparisonEnabled,
},
} = useApmParams('/mobile-services/{serviceName}/overview');
@ -142,11 +143,6 @@ export function MobileServiceOverview() {
</EuiCallOut>
<EuiSpacer size="s" />
</EuiFlexItem>
{fallbackToTransactions && (
<EuiFlexItem>
<AggregatedTransactionsBadge />
</EuiFlexItem>
)}
<EuiFlexItem>
<MobileStats
start={start}
@ -163,24 +159,46 @@ export function MobileServiceOverview() {
end={end}
kuery={kueryWithMobileFilters}
filters={embeddableFilters}
comparisonEnabled={comparisonEnabled}
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={7}>
<EuiPanel hasBorder={true}>
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.mostUsedTitle',
{
defaultMessage: 'Most used',
}
)}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexGroup
justifyContent="spaceBetween"
alignItems="center"
>
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.mostUsedTitle',
{
defaultMessage: 'Top 5 most used',
}
)}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{comparisonEnabled && (
<EuiIconTip
content={i18n.translate(
'xpack.apm.comparison.not.support',
{
defaultMessage: 'Comparison is not supported',
}
)}
size="m"
type="alert"
color="warning"
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup direction={rowDirection} gutterSize="s">
{/* Device */}
<EuiFlexItem>

View file

@ -6,7 +6,13 @@
*/
import React from 'react';
import { EuiTitle, EuiSpacer } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTitle,
EuiIconTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { Filter } from '@kbn/es-query';
import { EmbeddedMap } from './embedded_map';
@ -16,21 +22,39 @@ export function LatencyMap({
end,
kuery,
filters,
comparisonEnabled,
}: {
start: string;
end: string;
kuery?: string;
filters: Filter[];
comparisonEnabled: boolean;
}) {
return (
<>
<EuiTitle size="xs">
<h3>
{i18n.translate('xpack.apm.serviceOverview.embeddedMap.title', {
defaultMessage: 'Average latency per country',
})}
</h3>
</EuiTitle>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.apm.serviceOverview.embeddedMap.title', {
defaultMessage: 'Average latency per country',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{comparisonEnabled && (
<EuiIconTip
content={i18n.translate('xpack.apm.comparison.not.support', {
defaultMessage: 'Comparison is not supported',
})}
size="m"
type="alert"
color="warning"
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EmbeddedMap start={start} end={end} kuery={kuery} filters={filters} />
</>

View file

@ -6,30 +6,24 @@
*/
import { MetricDatum, MetricTrendShape } from '@elastic/charts';
import { i18n } from '@kbn/i18n';
import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import {
EuiIcon,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
} from '@elastic/eui';
import React, { useCallback } from 'react';
import { useTheme } from '@kbn/observability-plugin/public';
import { isEmpty } from 'lodash';
import { useAnyOfApmParams } from '../../../../../hooks/use_apm_params';
import { useFetcher, FETCH_STATUS } from '../../../../../hooks/use_fetcher';
import { MetricItem } from './metric_item';
import { usePreviousPeriodLabel } from '../../../../../hooks/use_previous_period_text';
const valueFormatter = (value: number, suffix = '') => {
return `${value} ${suffix}`;
};
const getIcon =
(type: string) =>
({
width = 20,
height = 20,
color,
}: {
width: number;
height: number;
color: string;
}) =>
<EuiIcon type={type} width={width} height={height} fill={color} />;
export function MobileStats({
start,
end,
@ -43,9 +37,11 @@ export function MobileStats({
const {
path: { serviceName },
query: { environment, transactionType },
query: { environment, transactionType, offset, comparisonEnabled },
} = useAnyOfApmParams('/mobile-services/{serviceName}/overview');
const previousPeriodLabel = usePreviousPeriodLabel();
const { data, status } = useFetcher(
(callApmApi) => {
return callApmApi(
@ -59,31 +55,71 @@ export function MobileStats({
environment,
kuery,
transactionType,
offset,
},
},
}
);
},
[start, end, environment, kuery, serviceName, transactionType]
[start, end, environment, kuery, serviceName, transactionType, offset]
);
const getComparisonValueFormatter = useCallback(
(value) => {
return (
<span>
{value && comparisonEnabled
? `${previousPeriodLabel}: ${value}`
: null}
</span>
);
},
[comparisonEnabled, previousPeriodLabel]
);
const getIcon = useCallback(
(type: string) =>
({
width = 20,
height = 20,
color,
}: {
width: number;
height: number;
color: string;
}) => {
return status === FETCH_STATUS.LOADING ? (
<EuiLoadingSpinner size="m" />
) : (
<EuiIcon type={type} width={width} height={height} fill={color} />
);
},
[status]
);
const metrics: MetricDatum[] = [
{
color: euiTheme.eui.euiColorLightestShade,
color: euiTheme.eui.euiColorDisabled,
title: i18n.translate('xpack.apm.mobile.metrics.crash.rate', {
defaultMessage: 'Crash Rate',
defaultMessage: 'Crash Rate (Crash per minute)',
}),
subtitle: i18n.translate('xpack.apm.mobile.coming.soon', {
defaultMessage: 'Coming Soon',
}),
icon: getIcon('bug'),
value: 'N/A',
valueFormatter: (value: number) => valueFormatter(value, 'cpm'),
valueFormatter: (value: number) => valueFormatter(value),
trend: [],
trendShape: MetricTrendShape.Area,
},
{
color: euiTheme.eui.euiColorLightestShade,
color: euiTheme.eui.euiColorDisabled,
title: i18n.translate('xpack.apm.mobile.metrics.load.time', {
defaultMessage: 'Slowest App load time',
}),
subtitle: i18n.translate('xpack.apm.mobile.coming.soon', {
defaultMessage: 'Coming Soon',
}),
icon: getIcon('visGauge'),
value: 'N/A',
valueFormatter: (value: number) => valueFormatter(value, 's'),
@ -99,6 +135,7 @@ export function MobileStats({
value: data?.currentPeriod?.sessions?.value ?? NaN,
valueFormatter: (value: number) => valueFormatter(value),
trend: data?.currentPeriod?.sessions?.timeseries,
extra: getComparisonValueFormatter(data?.previousPeriod.sessions?.value),
trendShape: MetricTrendShape.Area,
},
{
@ -108,8 +145,9 @@ export function MobileStats({
}),
icon: getIcon('kubernetesPod'),
value: data?.currentPeriod?.requests?.value ?? NaN,
extra: getComparisonValueFormatter(data?.previousPeriod.requests?.value),
valueFormatter: (value: number) => valueFormatter(value),
trend: data?.currentPeriod?.requests?.timeseries ?? [],
trend: data?.currentPeriod?.requests?.timeseries,
trendShape: MetricTrendShape.Area,
},
];
@ -121,7 +159,8 @@ export function MobileStats({
<MetricItem
id={key}
data={[metric]}
isLoading={status === FETCH_STATUS.LOADING}
hasData={!isEmpty(data)}
status={status}
/>
</EuiFlexItem>
))}

View file

@ -7,15 +7,18 @@
import React from 'react';
import { Chart, Metric, MetricDatum } from '@elastic/charts';
import { EuiLoadingContent, EuiPanel } from '@elastic/eui';
import { FETCH_STATUS, isPending } from '../../../../../hooks/use_fetcher';
export function MetricItem({
data,
id,
isLoading,
status,
hasData,
}: {
data: MetricDatum[];
id: number;
isLoading: boolean;
status: FETCH_STATUS;
hasData: boolean;
}) {
return (
<div
@ -23,11 +26,11 @@ export function MetricItem({
resize: 'none',
padding: '0px',
overflow: 'auto',
height: '100px',
height: '120px',
borderRadius: '6px',
}}
>
{isLoading ? (
{!hasData && isPending(status) ? (
<EuiPanel hasBorder={true}>
<EuiLoadingContent lines={3} />
</EuiPanel>

View file

@ -6,7 +6,6 @@
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiPanel,
@ -17,7 +16,6 @@ import { useHistory } from 'react-router-dom';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { AggregatedTransactionsBadge } from '../../../shared/aggregated_transactions_badge';
import { TransactionsTable } from '../../../shared/transactions_table';
import { replace } from '../../../shared/links/url_helpers';
import { getKueryWithMobileFilters } from '../../../../../common/utils/get_kuery_with_mobile_filters';
@ -51,7 +49,7 @@ export function MobileTransactionOverview() {
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const { transactionType, fallbackToTransactions } = useApmServiceContext();
const { transactionType } = useApmServiceContext();
const history = useHistory();
@ -65,16 +63,6 @@ export function MobileTransactionOverview() {
<EuiFlexItem>
<EuiHorizontalRule />
</EuiFlexItem>
{fallbackToTransactions && (
<>
<EuiFlexGroup>
<EuiFlexItem>
<AggregatedTransactionsBadge />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
</>
)}
<MobileTransactionCharts
transactionType={transactionType}
serviceName={serviceName}

View file

@ -67,6 +67,7 @@ const mobileStatsRoute = createApmServerRoute({
kueryRt,
rangeRt,
environmentRt,
offsetRt,
t.partial({
transactionType: t.string,
}),
@ -77,7 +78,7 @@ const mobileStatsRoute = createApmServerRoute({
const apmEventClient = await getApmEventClient(resources);
const { params } = resources;
const { serviceName } = params.path;
const { kuery, environment, start, end } = params.query;
const { kuery, environment, start, end, offset } = params.query;
const stats = await getMobileStatsPeriods({
kuery,
@ -86,6 +87,7 @@ const mobileStatsRoute = createApmServerRoute({
end,
serviceName,
apmEventClient,
offset,
});
return stats;

View file

@ -101,7 +101,9 @@ export async function generateMobileData({
.span({
spanName: 'onCreate',
spanType: 'app',
spanSubtype: 'internal',
spanSubtype: 'external',
'service.target.type': 'http',
'span.destination.service.resource': 'external',
})
.duration(50)
.success()

View file

@ -74,9 +74,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const response = await getHttpRequestsChart({ serviceName: 'synth-android', offset: '1d' });
expect(response.status).to.be(200);
expect(
response.body.currentPeriod.timeseries.some((item) => item.y === 0 && item.x)
).to.eql(true);
expect(response.body.currentPeriod.timeseries.some((item) => item.x && item.y)).to.eql(
true
);
expect(response.body.previousPeriod.timeseries[0].y).to.eql(0);
});
@ -88,7 +88,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
response.body.currentPeriod.timeseries.some((item) => item.y === 0 && item.x)
).to.eql(true);
expect(response.body.currentPeriod.timeseries[0].y).to.eql(0);
expect(response.body.currentPeriod.timeseries[0].y).to.eql(1);
expect(response.body.previousPeriod.timeseries).to.eql([]);
});
});