mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[APM] Add widgets for Most HTTP requests, Errors and Sessions, at the bottom of the Map layer/component in the service overview page (#149101)
Closes https://github.com/elastic/kibana/issues/146859 #### What was done - Improved maps UI - Added location widgets for most by country <img width="1777" alt="image" src="https://user-images.githubusercontent.com/31922082/214525608-a153ad78-0c1f-42d2-a473-857c9875c4d1.png">
This commit is contained in:
parent
867a55274c
commit
6f4b5fd5de
15 changed files with 879 additions and 138 deletions
|
@ -15,8 +15,14 @@ exports[`Error APP_LAUNCH_TIME 1`] = `undefined`;
|
|||
|
||||
exports[`Error CHILD_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Error CLIENT_GEO_CITY_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Error CLIENT_GEO_COUNTRY_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error CLIENT_GEO_REGION_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error CLOUD 1`] = `
|
||||
Object {
|
||||
"availability_zone": "europe-west1-c",
|
||||
|
@ -313,8 +319,14 @@ exports[`Span APP_LAUNCH_TIME 1`] = `undefined`;
|
|||
|
||||
exports[`Span CHILD_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Span CLIENT_GEO_CITY_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Span CLIENT_GEO_COUNTRY_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span CLIENT_GEO_REGION_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span CLOUD 1`] = `
|
||||
Object {
|
||||
"availability_zone": "europe-west1-c",
|
||||
|
@ -594,8 +606,14 @@ exports[`Transaction APP_LAUNCH_TIME 1`] = `undefined`;
|
|||
|
||||
exports[`Transaction CHILD_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CLIENT_GEO_CITY_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CLIENT_GEO_COUNTRY_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CLIENT_GEO_REGION_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CLOUD 1`] = `
|
||||
Object {
|
||||
"availability_zone": "europe-west1-c",
|
||||
|
|
|
@ -150,8 +150,6 @@ export const KUBERNETES = 'kubernetes';
|
|||
export const KUBERNETES_POD_NAME = 'kubernetes.pod.name';
|
||||
export const KUBERNETES_POD_UID = 'kubernetes.pod.uid';
|
||||
|
||||
export const CLIENT_GEO_COUNTRY_ISO_CODE = 'client.geo.country_iso_code';
|
||||
|
||||
export const FAAS_ID = 'faas.id';
|
||||
export const FAAS_NAME = 'faas.name';
|
||||
export const FAAS_COLDSTART = 'faas.coldstart';
|
||||
|
@ -171,4 +169,10 @@ export const SESSION_ID = 'session.id';
|
|||
export const APP_LAUNCH_TIME = 'application.launch.time';
|
||||
export const EVENT_NAME = 'event.name';
|
||||
|
||||
// Location
|
||||
export const CLIENT_GEO_COUNTRY_ISO_CODE = 'client.geo.country_iso_code';
|
||||
export const CLIENT_GEO_COUNTRY_NAME = 'client.geo.country_name';
|
||||
export const CLIENT_GEO_CITY_NAME = 'client.geo.city_name';
|
||||
export const CLIENT_GEO_REGION_NAME = 'client.geo.region_name';
|
||||
|
||||
export const CHILD_ID = 'child.id';
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiCallOut,
|
||||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -42,7 +41,8 @@ import { ServiceOverviewDependenciesTable } from '../../service_overview/service
|
|||
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';
|
||||
import { MobileStats } from './stats';
|
||||
import { MobileStats } from './stats/stats';
|
||||
import { MobileLocationStats } from './stats/location_stats';
|
||||
/**
|
||||
* The height a chart should be if it's next to a table with 5 rows and a title.
|
||||
* Add the height of the pagination row.
|
||||
|
@ -149,10 +149,11 @@ export function MobileServiceOverview() {
|
|||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={5}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={8}>
|
||||
<EuiPanel hasBorder={true}>
|
||||
<LatencyMap
|
||||
start={start}
|
||||
|
@ -163,114 +164,106 @@ export function MobileServiceOverview() {
|
|||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={7}>
|
||||
<EuiPanel hasBorder={true}>
|
||||
<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>
|
||||
<MostUsedChart
|
||||
title={i18n.translate(
|
||||
'xpack.apm.serviceOverview.mostUsed.device',
|
||||
{
|
||||
defaultMessage: 'Devices',
|
||||
}
|
||||
)}
|
||||
metric={DEVICE_MODEL_IDENTIFIER}
|
||||
start={start}
|
||||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
filters={embeddableFilters}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{/* NCT */}
|
||||
<EuiFlexItem>
|
||||
<MostUsedChart
|
||||
title={i18n.translate(
|
||||
'xpack.apm.serviceOverview.mostUsed.nct',
|
||||
{
|
||||
defaultMessage: 'Network Connection Type',
|
||||
}
|
||||
)}
|
||||
metric={NETWORK_CONNECTION_TYPE}
|
||||
start={start}
|
||||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
filters={embeddableFilters}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
{/* OS version */}
|
||||
<EuiFlexItem>
|
||||
<MostUsedChart
|
||||
title={i18n.translate(
|
||||
'xpack.apm.serviceOverview.mostUsed.osVersion',
|
||||
{
|
||||
defaultMessage: 'OS version',
|
||||
}
|
||||
)}
|
||||
metric={HOST_OS_VERSION}
|
||||
start={start}
|
||||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
filters={embeddableFilters}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{/* App version */}
|
||||
<EuiFlexItem>
|
||||
<MostUsedChart
|
||||
title={i18n.translate(
|
||||
'xpack.apm.serviceOverview.mostUsed.appVersion',
|
||||
{
|
||||
defaultMessage: 'App version',
|
||||
}
|
||||
)}
|
||||
metric={SERVICE_VERSION}
|
||||
start={start}
|
||||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
filters={embeddableFilters}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
<EuiFlexItem grow={4}>
|
||||
<MobileLocationStats
|
||||
start={start}
|
||||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasBorder={true} color="subdued">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.serviceOverview.mostUsedTitle', {
|
||||
defaultMessage: 'Most used',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiFlexGroup direction={rowDirection} gutterSize="s">
|
||||
{/* Device */}
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasBorder={true}>
|
||||
<MostUsedChart
|
||||
title={i18n.translate(
|
||||
'xpack.apm.serviceOverview.mostUsed.device',
|
||||
{
|
||||
defaultMessage: 'Devices',
|
||||
}
|
||||
)}
|
||||
metric={DEVICE_MODEL_IDENTIFIER}
|
||||
start={start}
|
||||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
filters={embeddableFilters}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
{/* NCT */}
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasBorder={true}>
|
||||
<MostUsedChart
|
||||
title={i18n.translate(
|
||||
'xpack.apm.serviceOverview.mostUsed.nct',
|
||||
{
|
||||
defaultMessage: 'Network Connection Type',
|
||||
}
|
||||
)}
|
||||
metric={NETWORK_CONNECTION_TYPE}
|
||||
start={start}
|
||||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
filters={embeddableFilters}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
{/* OS version */}
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasBorder={true}>
|
||||
<MostUsedChart
|
||||
title={i18n.translate(
|
||||
'xpack.apm.serviceOverview.mostUsed.osVersion',
|
||||
{
|
||||
defaultMessage: 'OS version',
|
||||
}
|
||||
)}
|
||||
metric={HOST_OS_VERSION}
|
||||
start={start}
|
||||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
filters={embeddableFilters}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
{/* App version */}
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasBorder={true}>
|
||||
<MostUsedChart
|
||||
title={i18n.translate(
|
||||
'xpack.apm.serviceOverview.mostUsed.appVersion',
|
||||
{
|
||||
defaultMessage: 'App version',
|
||||
}
|
||||
)}
|
||||
metric={SERVICE_VERSION}
|
||||
start={start}
|
||||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
filters={embeddableFilters}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasBorder={true}>
|
||||
<LatencyChart
|
||||
|
|
|
@ -92,6 +92,7 @@ function EmbeddedMapComponent({
|
|||
),
|
||||
filters,
|
||||
viewMode: ViewMode.VIEW,
|
||||
mapCenter: { lat: 20.43425, lon: 0, zoom: 1.25 },
|
||||
isLayerTOCOpen: false,
|
||||
query: {
|
||||
query: kuery,
|
||||
|
@ -157,7 +158,7 @@ function EmbeddedMapComponent({
|
|||
data-test-subj="serviceOverviewEmbeddedMap"
|
||||
css={css`
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
height: 500px;
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
z-index: 1;
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { MetricDatum, MetricTrendShape } from '@elastic/charts';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTheme } from '@kbn/observability-plugin/public';
|
||||
import { useAnyOfApmParams } from '../../../../../hooks/use_apm_params';
|
||||
import { useFetcher, isPending } from '../../../../../hooks/use_fetcher';
|
||||
import { CLIENT_GEO_COUNTRY_NAME } from '../../../../../../common/es_fields/apm';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../../common/i18n';
|
||||
import { MetricItem } from './metric_item';
|
||||
import { usePreviousPeriodLabel } from '../../../../../hooks/use_previous_period_text';
|
||||
|
||||
const getIcon =
|
||||
(type: string) =>
|
||||
({
|
||||
width = 20,
|
||||
height = 20,
|
||||
color,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
color: string;
|
||||
}) =>
|
||||
<EuiIcon type={type} width={width} height={height} fill={color} />;
|
||||
|
||||
const formatDifference = (value: number) => {
|
||||
return value > 0 ? '+' + value.toFixed(0) + '%' : value.toFixed(0) + '%';
|
||||
};
|
||||
|
||||
const calculateDiffPercentageAndFormat = (
|
||||
currentValue?: number,
|
||||
previousValue?: number
|
||||
) => {
|
||||
if (currentValue && previousValue) {
|
||||
const diffPercentageValue =
|
||||
((currentValue - previousValue) / previousValue) * 100;
|
||||
|
||||
return formatDifference(diffPercentageValue);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export function MobileLocationStats({
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
}: {
|
||||
start: string;
|
||||
end: string;
|
||||
kuery: string;
|
||||
}) {
|
||||
const euiTheme = useTheme();
|
||||
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: { environment, offset, comparisonEnabled },
|
||||
} = useAnyOfApmParams('/mobile-services/{serviceName}/overview');
|
||||
|
||||
const previousPeriodLabel = usePreviousPeriodLabel();
|
||||
|
||||
const locationField = CLIENT_GEO_COUNTRY_NAME;
|
||||
|
||||
const { data: locationStatsData, status: locationStatsStatus } = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/mobile-services/{serviceName}/location/stats',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
locationField,
|
||||
offset,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
[start, end, environment, kuery, serviceName, locationField, offset]
|
||||
);
|
||||
|
||||
const loadingLocationStats = isPending(locationStatsStatus);
|
||||
|
||||
const currentPeriod = locationStatsData?.currentPeriod;
|
||||
const previousPeriod = locationStatsData?.previousPeriod;
|
||||
|
||||
const getComparisonValueFormatter = useCallback(
|
||||
({ currentPeriodValue, previousPeriodValue }) => {
|
||||
const comparisonDiffValue = calculateDiffPercentageAndFormat(
|
||||
currentPeriodValue,
|
||||
previousPeriodValue
|
||||
);
|
||||
if (comparisonDiffValue && comparisonEnabled) {
|
||||
return (
|
||||
<span>
|
||||
{currentPeriodValue} ({comparisonDiffValue} {previousPeriodLabel})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span>{currentPeriodValue ? `${currentPeriodValue}` : null}</span>;
|
||||
},
|
||||
[comparisonEnabled, previousPeriodLabel]
|
||||
);
|
||||
|
||||
const metrics: MetricDatum[] = [
|
||||
{
|
||||
color: euiTheme.eui.euiColorLightestShade,
|
||||
title: i18n.translate(
|
||||
'xpack.apm.mobile.location.metrics.http.requests.title',
|
||||
{
|
||||
defaultMessage: 'Most used in',
|
||||
}
|
||||
),
|
||||
extra: getComparisonValueFormatter({
|
||||
currentPeriodValue: currentPeriod?.mostRequests.value,
|
||||
previousPeriodValue: previousPeriod?.mostRequests.value,
|
||||
}),
|
||||
icon: getIcon('visBarHorizontal'),
|
||||
value: currentPeriod?.mostRequests.location ?? NOT_AVAILABLE_LABEL,
|
||||
valueFormatter: (value) => `${value}`,
|
||||
trend: currentPeriod?.mostRequests.timeseries,
|
||||
trendShape: MetricTrendShape.Area,
|
||||
},
|
||||
{
|
||||
color: euiTheme.eui.euiColorDisabled,
|
||||
title: i18n.translate('xpack.apm.mobile.location.metrics.crashes', {
|
||||
defaultMessage: 'Most crashes',
|
||||
}),
|
||||
subtitle: i18n.translate('xpack.apm.mobile.coming.soon', {
|
||||
defaultMessage: 'Coming Soon',
|
||||
}),
|
||||
icon: getIcon('bug'),
|
||||
value: NOT_AVAILABLE_LABEL,
|
||||
valueFormatter: (value) => `${value}`,
|
||||
trend: [],
|
||||
trendShape: MetricTrendShape.Area,
|
||||
},
|
||||
{
|
||||
color: euiTheme.eui.euiColorLightestShade,
|
||||
title: i18n.translate('xpack.apm.mobile.location.metrics.sessions', {
|
||||
defaultMessage: 'Most sessions',
|
||||
}),
|
||||
extra: getComparisonValueFormatter({
|
||||
currentPeriodValue: currentPeriod?.mostSessions.value,
|
||||
previousPeriodValue: previousPeriod?.mostSessions.value,
|
||||
}),
|
||||
icon: getIcon('timeslider'),
|
||||
value: currentPeriod?.mostSessions.location ?? NOT_AVAILABLE_LABEL,
|
||||
valueFormatter: (value) => `${value}`,
|
||||
trend: currentPeriod?.mostSessions.timeseries,
|
||||
trendShape: MetricTrendShape.Area,
|
||||
},
|
||||
{
|
||||
color: euiTheme.eui.euiColorDisabled,
|
||||
title: i18n.translate('xpack.apm.mobile.location.metrics.launches', {
|
||||
defaultMessage: 'Most launches',
|
||||
}),
|
||||
subtitle: i18n.translate('xpack.apm.mobile.coming.soon', {
|
||||
defaultMessage: 'Coming Soon',
|
||||
}),
|
||||
icon: getIcon('launch'),
|
||||
value: NOT_AVAILABLE_LABEL,
|
||||
valueFormatter: (value) => `${value}`,
|
||||
trend: [],
|
||||
trendShape: MetricTrendShape.Area,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
{metrics.map((metric, key) => (
|
||||
<EuiFlexItem key={key}>
|
||||
<MetricItem
|
||||
id={key}
|
||||
data={[metric]}
|
||||
isLoading={loadingLocationStats}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -7,30 +7,31 @@
|
|||
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';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
export function MetricItem({
|
||||
data,
|
||||
id,
|
||||
status,
|
||||
hasData,
|
||||
isLoading,
|
||||
height = '124px',
|
||||
}: {
|
||||
data: MetricDatum[];
|
||||
id: number;
|
||||
status: FETCH_STATUS;
|
||||
hasData: boolean;
|
||||
isLoading: boolean;
|
||||
height?: string;
|
||||
}) {
|
||||
const hasData = !isEmpty(data);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
resize: 'none',
|
||||
padding: '0px',
|
||||
overflow: 'auto',
|
||||
height: '120px',
|
||||
height,
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
>
|
||||
{!hasData && isPending(status) ? (
|
||||
{!hasData && isLoading ? (
|
||||
<EuiPanel hasBorder={true}>
|
||||
<EuiLoadingContent lines={3} />
|
||||
</EuiPanel>
|
||||
|
|
|
@ -14,9 +14,12 @@ import {
|
|||
} 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 {
|
||||
useFetcher,
|
||||
FETCH_STATUS,
|
||||
isPending,
|
||||
} from '../../../../../hooks/use_fetcher';
|
||||
import { MetricItem } from './metric_item';
|
||||
import { usePreviousPeriodLabel } from '../../../../../hooks/use_previous_period_text';
|
||||
|
||||
|
@ -97,6 +100,8 @@ export function MobileStats({
|
|||
[status]
|
||||
);
|
||||
|
||||
const loadingStats = isPending(status);
|
||||
|
||||
const metrics: MetricDatum[] = [
|
||||
{
|
||||
color: euiTheme.eui.euiColorDisabled,
|
||||
|
@ -155,13 +160,8 @@ export function MobileStats({
|
|||
return (
|
||||
<EuiFlexGroup>
|
||||
{metrics.map((metric, key) => (
|
||||
<EuiFlexItem>
|
||||
<MetricItem
|
||||
id={key}
|
||||
data={[metric]}
|
||||
hasData={!isEmpty(data)}
|
||||
status={status}
|
||||
/>
|
||||
<EuiFlexItem key={key}>
|
||||
<MetricItem id={key} data={[metric]} isLoading={loadingStats} />
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
|
@ -13,8 +13,9 @@ import {
|
|||
} from '@kbn/observability-plugin/server';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
SERVICE_TARGET_TYPE,
|
||||
TRANSACTION_NAME,
|
||||
SPAN_TYPE,
|
||||
SPAN_SUBTYPE,
|
||||
} from '../../../common/es_fields/apm';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import { getBucketSize } from '../../../common/utils/get_bucket_size';
|
||||
|
@ -23,7 +24,6 @@ import { offsetPreviousPeriodCoordinates } from '../../../common/utils/offset_pr
|
|||
import { Maybe } from '../../../typings/common';
|
||||
import { Coordinate } from '../../../typings/timeseries';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { getDocumentTypeFilterForServiceDestinationStatistics } from '../../lib/helpers/spans/get_is_using_service_destination_metrics';
|
||||
|
||||
export interface HttpRequestsTimeseries {
|
||||
currentPeriod: { timeseries: Coordinate[]; value: Maybe<number> };
|
||||
|
@ -64,21 +64,21 @@ async function getHttpRequestsTimeseries({
|
|||
|
||||
const aggs = {
|
||||
requests: {
|
||||
filter: { term: { [SERVICE_TARGET_TYPE]: 'http' } },
|
||||
filter: { term: { [SPAN_SUBTYPE]: 'http' } },
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search('get_http_requests_chart', {
|
||||
apm: { events: [ProcessorEvent.metric] },
|
||||
apm: { events: [ProcessorEvent.span] },
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ exists: { field: SERVICE_TARGET_TYPE } },
|
||||
...getDocumentTypeFilterForServiceDestinationStatistics(true),
|
||||
{ exists: { field: SPAN_SUBTYPE } },
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...termQuery(SPAN_TYPE, 'external'),
|
||||
...termQuery(TRANSACTION_NAME, transactionName),
|
||||
...rangeQuery(startWithOffset, endWithOffset),
|
||||
...environmentQuery(environment),
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
termQuery,
|
||||
kqlQuery,
|
||||
rangeQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
SPAN_TYPE,
|
||||
SPAN_SUBTYPE,
|
||||
} from '../../../common/es_fields/apm';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms';
|
||||
import { getBucketSize } from '../../../common/utils/get_bucket_size';
|
||||
|
||||
interface Props {
|
||||
kuery: string;
|
||||
apmEventClient: APMEventClient;
|
||||
serviceName: string;
|
||||
environment: string;
|
||||
start: number;
|
||||
end: number;
|
||||
locationField?: string;
|
||||
offset?: string;
|
||||
}
|
||||
|
||||
export async function getHttpRequestsByLocation({
|
||||
kuery,
|
||||
apmEventClient,
|
||||
serviceName,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
locationField,
|
||||
offset,
|
||||
}: Props) {
|
||||
const { startWithOffset, endWithOffset } = getOffsetInMs({
|
||||
start,
|
||||
end,
|
||||
offset,
|
||||
});
|
||||
|
||||
const { intervalString } = getBucketSize({
|
||||
start: startWithOffset,
|
||||
end: endWithOffset,
|
||||
minBucketSize: 60,
|
||||
});
|
||||
|
||||
const aggs = {
|
||||
requests: {
|
||||
filter: { term: { [SPAN_SUBTYPE]: 'http' } },
|
||||
aggs: {
|
||||
requestsByLocation: {
|
||||
terms: {
|
||||
field: locationField,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search(
|
||||
'get_mobile_location_http_requests',
|
||||
{
|
||||
apm: {
|
||||
events: [ProcessorEvent.span],
|
||||
},
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...termQuery(SPAN_TYPE, 'external'),
|
||||
...rangeQuery(startWithOffset, endWithOffset),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
timeseries: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: intervalString,
|
||||
min_doc_count: 0,
|
||||
},
|
||||
aggs,
|
||||
},
|
||||
...aggs,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
location: response.aggregations?.requests?.requestsByLocation?.buckets[0]
|
||||
?.key as string,
|
||||
value:
|
||||
response.aggregations?.requests?.requestsByLocation?.buckets[0]
|
||||
?.doc_count ?? 0,
|
||||
timeseries:
|
||||
response.aggregations?.timeseries?.buckets.map((bucket) => ({
|
||||
x: bucket.key,
|
||||
y:
|
||||
response.aggregations?.requests?.requestsByLocation?.buckets[0]
|
||||
?.doc_count ?? 0,
|
||||
})) ?? [],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CLIENT_GEO_COUNTRY_NAME } from '../../../common/es_fields/apm';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { getSessionsByLocation } from './get_mobile_sessions_by_location';
|
||||
import { getHttpRequestsByLocation } from './get_mobile_http_requests_by_location';
|
||||
import { Maybe } from '../../../typings/common';
|
||||
|
||||
export type Timeseries = Array<{ x: number; y: number }>;
|
||||
|
||||
interface LocationStats {
|
||||
mostSessions: {
|
||||
location?: string;
|
||||
value: Maybe<number>;
|
||||
timeseries: Timeseries;
|
||||
};
|
||||
mostRequests: {
|
||||
location?: string;
|
||||
value: Maybe<number>;
|
||||
timeseries: Timeseries;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MobileLocationStats {
|
||||
currentPeriod: LocationStats;
|
||||
previousPeriod: LocationStats;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
kuery: string;
|
||||
apmEventClient: APMEventClient;
|
||||
serviceName: string;
|
||||
environment: string;
|
||||
start: number;
|
||||
end: number;
|
||||
locationField?: string;
|
||||
offset?: string;
|
||||
}
|
||||
|
||||
async function getMobileLocationStats({
|
||||
kuery,
|
||||
apmEventClient,
|
||||
serviceName,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
locationField = CLIENT_GEO_COUNTRY_NAME,
|
||||
offset,
|
||||
}: Props) {
|
||||
const commonProps = {
|
||||
kuery,
|
||||
apmEventClient,
|
||||
serviceName,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
locationField,
|
||||
offset,
|
||||
};
|
||||
|
||||
const [mostSessions, mostRequests] = await Promise.all([
|
||||
getSessionsByLocation({ ...commonProps }),
|
||||
getHttpRequestsByLocation({ ...commonProps }),
|
||||
]);
|
||||
|
||||
return {
|
||||
mostSessions,
|
||||
mostRequests,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getMobileLocationStatsPeriods({
|
||||
kuery,
|
||||
apmEventClient,
|
||||
serviceName,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
locationField,
|
||||
offset,
|
||||
}: Props): Promise<MobileLocationStats> {
|
||||
const commonProps = {
|
||||
kuery,
|
||||
apmEventClient,
|
||||
serviceName,
|
||||
environment,
|
||||
locationField,
|
||||
};
|
||||
|
||||
const currentPeriodPromise = getMobileLocationStats({
|
||||
...commonProps,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
const previousPeriodPromise = offset
|
||||
? getMobileLocationStats({
|
||||
...commonProps,
|
||||
start,
|
||||
end,
|
||||
offset,
|
||||
})
|
||||
: {
|
||||
mostSessions: { value: null, timeseries: [] },
|
||||
mostRequests: { value: null, timeseries: [] },
|
||||
};
|
||||
|
||||
const [currentPeriod, previousPeriod] = await Promise.all([
|
||||
currentPeriodPromise,
|
||||
previousPeriodPromise,
|
||||
]);
|
||||
|
||||
return {
|
||||
currentPeriod,
|
||||
previousPeriod,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
termQuery,
|
||||
kqlQuery,
|
||||
rangeQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import { SERVICE_NAME, SESSION_ID } from '../../../common/es_fields/apm';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms';
|
||||
import { getBucketSize } from '../../../common/utils/get_bucket_size';
|
||||
|
||||
interface Props {
|
||||
kuery: string;
|
||||
apmEventClient: APMEventClient;
|
||||
serviceName: string;
|
||||
environment: string;
|
||||
start: number;
|
||||
end: number;
|
||||
locationField?: string;
|
||||
offset?: string;
|
||||
}
|
||||
|
||||
export async function getSessionsByLocation({
|
||||
kuery,
|
||||
apmEventClient,
|
||||
serviceName,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
locationField,
|
||||
offset,
|
||||
}: Props) {
|
||||
const { startWithOffset, endWithOffset } = getOffsetInMs({
|
||||
start,
|
||||
end,
|
||||
offset,
|
||||
});
|
||||
|
||||
const { intervalString } = getBucketSize({
|
||||
start: startWithOffset,
|
||||
end: endWithOffset,
|
||||
minBucketSize: 60,
|
||||
});
|
||||
|
||||
const aggs = {
|
||||
sessions: {
|
||||
terms: {
|
||||
field: locationField,
|
||||
},
|
||||
aggs: {
|
||||
sessions: {
|
||||
cardinality: { field: SESSION_ID },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search('get_mobile_location_sessions', {
|
||||
apm: {
|
||||
events: [ProcessorEvent.transaction],
|
||||
},
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...rangeQuery(startWithOffset, endWithOffset),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
timeseries: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: intervalString,
|
||||
min_doc_count: 0,
|
||||
},
|
||||
aggs,
|
||||
},
|
||||
...aggs,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
location: response.aggregations?.sessions?.buckets[0]?.key as string,
|
||||
value: response.aggregations?.sessions?.buckets[0]?.sessions.value ?? 0,
|
||||
timeseries:
|
||||
response.aggregations?.timeseries?.buckets.map((bucket) => ({
|
||||
x: bucket.key,
|
||||
y: bucket.sessions.buckets[0]?.sessions.value ?? 0,
|
||||
})) ?? [],
|
||||
};
|
||||
}
|
|
@ -17,6 +17,10 @@ import {
|
|||
import { getMobileFilters } from './get_mobile_filters';
|
||||
import { getMobileSessions, SessionsTimeseries } from './get_mobile_sessions';
|
||||
import { getMobileStatsPeriods, MobilePeriodStats } from './get_mobile_stats';
|
||||
import {
|
||||
getMobileLocationStatsPeriods,
|
||||
MobileLocationStats,
|
||||
} from './get_mobile_location_stats';
|
||||
|
||||
const mobileFiltersRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/mobile/filters',
|
||||
|
@ -94,6 +98,45 @@ const mobileStatsRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const mobileLocationStatsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/location/stats',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.intersection([
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
environmentRt,
|
||||
offsetRt,
|
||||
t.partial({
|
||||
locationField: t.string,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (resources): Promise<MobileLocationStats> => {
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const { kuery, environment, start, end, locationField, offset } =
|
||||
params.query;
|
||||
|
||||
const locationStats = await getMobileLocationStatsPeriods({
|
||||
kuery,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
serviceName,
|
||||
apmEventClient,
|
||||
locationField,
|
||||
offset,
|
||||
});
|
||||
|
||||
return locationStats;
|
||||
},
|
||||
});
|
||||
|
||||
const sessionsChartRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /internal/apm/mobile-services/{serviceName}/transactions/charts/sessions',
|
||||
|
@ -181,4 +224,5 @@ export const mobileRouteRepository = {
|
|||
...sessionsChartRoute,
|
||||
...httpRequestsChartRoute,
|
||||
...mobileStatsRoute,
|
||||
...mobileLocationStatsRoute,
|
||||
};
|
||||
|
|
|
@ -144,7 +144,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()
|
||||
|
|
|
@ -92,7 +92,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(1);
|
||||
expect(response.body.currentPeriod.timeseries[0].y).to.eql(3);
|
||||
expect(response.body.previousPeriod.timeseries).to.eql([]);
|
||||
});
|
||||
});
|
||||
|
@ -125,8 +125,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(response.status).to.be(200);
|
||||
expect(ntcCell.status).to.be(200);
|
||||
expect(response.body.currentPeriod.timeseries[0].y).to.eql(0);
|
||||
expect(ntcCell.body.currentPeriod.timeseries[0].y).to.eql(0);
|
||||
expect(response.body.currentPeriod.timeseries[0].y).to.eql(2);
|
||||
expect(ntcCell.body.currentPeriod.timeseries[0].y).to.eql(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { generateMobileData } from './generate_mobile_data';
|
||||
|
||||
type MobileLocationStats =
|
||||
APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/location/stats'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const registry = getService('registry');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
|
||||
const start = new Date('2023-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1;
|
||||
|
||||
async function getMobileLocationStats({
|
||||
environment = ENVIRONMENT_ALL.value,
|
||||
kuery = '',
|
||||
serviceName,
|
||||
locationField = 'client.geo.country_name',
|
||||
}: {
|
||||
environment?: string;
|
||||
kuery?: string;
|
||||
serviceName: string;
|
||||
locationField?: string;
|
||||
}) {
|
||||
return await apmApiClient
|
||||
.readUser({
|
||||
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/location/stats',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
environment,
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
kuery,
|
||||
locationField,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ body }) => body);
|
||||
}
|
||||
|
||||
registry.when('Location stats when data is not loaded', { config: 'basic', archives: [] }, () => {
|
||||
describe('when no data', () => {
|
||||
it('handles empty state', async () => {
|
||||
const response = await getMobileLocationStats({ serviceName: 'foo' });
|
||||
expect(response.currentPeriod.mostSessions.timeseries.every((item) => item.y === 0)).to.eql(
|
||||
true
|
||||
);
|
||||
expect(response.currentPeriod.mostRequests.timeseries.every((item) => item.y === 0)).to.eql(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
registry.when('Location stats', { config: 'basic', archives: [] }, () => {
|
||||
before(async () => {
|
||||
await generateMobileData({
|
||||
synthtraceEsClient,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
let response: MobileLocationStats;
|
||||
|
||||
before(async () => {
|
||||
response = await getMobileLocationStats({
|
||||
serviceName: 'synth-android',
|
||||
environment: 'production',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns location for most sessions', () => {
|
||||
const { location } = response.currentPeriod.mostSessions;
|
||||
expect(location).to.be('China');
|
||||
});
|
||||
|
||||
it('returns location for most requests', () => {
|
||||
const { location } = response.currentPeriod.mostRequests;
|
||||
expect(location).to.be('China');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when filters are applied', () => {
|
||||
it('returns empty state for filters', async () => {
|
||||
const response = await getMobileLocationStats({
|
||||
serviceName: 'synth-android',
|
||||
environment: 'production',
|
||||
kuery: `app.version:"none"`,
|
||||
});
|
||||
|
||||
expect(response.currentPeriod.mostSessions.value).to.eql(0);
|
||||
expect(response.currentPeriod.mostRequests.value).to.eql(0);
|
||||
|
||||
expect(response.currentPeriod.mostSessions.timeseries.every((item) => item.y === 0)).to.eql(
|
||||
true
|
||||
);
|
||||
expect(response.currentPeriod.mostRequests.timeseries.every((item) => item.y === 0)).to.eql(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the correct values when single filter is applied', async () => {
|
||||
const response = await getMobileLocationStats({
|
||||
serviceName: 'synth-android',
|
||||
environment: 'production',
|
||||
kuery: `service.version:"1.0"`,
|
||||
});
|
||||
|
||||
expect(response.currentPeriod.mostSessions.value).to.eql(6);
|
||||
expect(response.currentPeriod.mostRequests.value).to.eql(0);
|
||||
});
|
||||
|
||||
it('returns the correct values when multiple filters are applied', async () => {
|
||||
const response = await getMobileLocationStats({
|
||||
serviceName: 'synth-android',
|
||||
kuery: `service.version:"1.0" and service.environment: "production"`,
|
||||
});
|
||||
|
||||
expect(response.currentPeriod.mostSessions.value).to.eql(6);
|
||||
expect(response.currentPeriod.mostRequests.value).to.eql(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue