[APM] Adding comparison to Throughput chart, Error rate chart, and Errors table (#94204)

* adding comparison to throuput chart

* adding comparison to error rate chart

* adding comparison to errors table

* fixing/adding api test

* addressing pr comments

* addressing pr comments

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2021-03-16 09:12:50 -04:00 committed by GitHub
parent 6c9cfd4893
commit b0fa077e8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1433 additions and 110 deletions

View file

@ -22,9 +22,11 @@ type ErrorGroupComparisonStatistics = APIReturnType<'GET /api/apm/services/{serv
export function getColumns({
serviceName,
errorGroupComparisonStatistics,
comparisonEnabled,
}: {
serviceName: string;
errorGroupComparisonStatistics: ErrorGroupComparisonStatistics;
comparisonEnabled?: boolean;
}): Array<EuiBasicTableColumn<ErrorGroupPrimaryStatistics['error_groups'][0]>> {
return [
{
@ -71,12 +73,17 @@ export function getColumns({
),
width: px(unit * 12),
render: (_, { occurrences, group_id: errorGroupId }) => {
const timeseries =
errorGroupComparisonStatistics?.[errorGroupId]?.timeseries;
const currentPeriodTimeseries =
errorGroupComparisonStatistics?.currentPeriod?.[errorGroupId]
?.timeseries;
const previousPeriodTimeseries =
errorGroupComparisonStatistics?.previousPeriod?.[errorGroupId]
?.timeseries;
return (
<SparkPlot
color="euiColorVis7"
series={timeseries}
series={currentPeriodTimeseries}
valueLabel={i18n.translate(
'xpack.apm.serviceOveriew.errorsTableOccurrences',
{
@ -86,6 +93,9 @@ export function getColumns({
},
}
)}
comparisonSeries={
comparisonEnabled ? previousPeriodTimeseries : undefined
}
/>
);
},

View file

@ -18,14 +18,18 @@ import uuid from 'uuid';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink';
import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper';
import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison';
import { ServiceOverviewTableContainer } from '../service_overview_table_container';
import { getColumns } from './get_column';
interface Props {
serviceName: string;
}
type ErrorGroupPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/primary_statistics'>;
type ErrorGroupComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics'>;
type SortDirection = 'asc' | 'desc';
type SortField = 'name' | 'last_seen' | 'occurrences';
@ -36,14 +40,31 @@ const DEFAULT_SORT = {
field: 'occurrences' as const,
};
const INITIAL_STATE = {
const INITIAL_STATE_PRIMARY_STATISTICS: {
items: ErrorGroupPrimaryStatistics['error_groups'];
totalItems: number;
requestId?: string;
} = {
items: [],
totalItems: 0,
requestId: undefined,
};
const INITIAL_STATE_COMPARISON_STATISTICS: ErrorGroupComparisonStatistics = {
currentPeriod: {},
previousPeriod: {},
};
export function ServiceOverviewErrorsTable({ serviceName }: Props) {
const {
urlParams: { environment, kuery, start, end },
urlParams: {
environment,
kuery,
start,
end,
comparisonType,
comparisonEnabled,
},
} = useUrlParams();
const { transactionType } = useApmServiceContext();
const [tableOptions, setTableOptions] = useState<{
@ -57,9 +78,16 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
sort: DEFAULT_SORT,
});
const { pageIndex, sort } = tableOptions;
const { comparisonStart, comparisonEnd } = getTimeRangeComparison({
start,
end,
comparisonType,
});
const { data = INITIAL_STATE, status } = useFetcher(
const { pageIndex, sort } = tableOptions;
const { direction, field } = sort;
const { data = INITIAL_STATE_PRIMARY_STATISTICS, status } = useFetcher(
(callApmApi) => {
if (!start || !end || !transactionType) {
return;
@ -78,37 +106,43 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
},
},
}).then((response) => {
const currentPageErrorGroups = orderBy(
response.error_groups,
field,
direction
).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE);
return {
requestId: uuid(),
items: response.error_groups,
items: currentPageErrorGroups,
totalItems: response.error_groups.length,
};
});
},
[environment, kuery, start, end, serviceName, transactionType]
// comparisonType is listed as dependency even thought it is not used. This is needed to trigger the comparison api when it is changed.
// eslint-disable-next-line react-hooks/exhaustive-deps
[
environment,
kuery,
start,
end,
serviceName,
transactionType,
pageIndex,
direction,
field,
comparisonType,
]
);
const { requestId, items } = data;
const currentPageErrorGroups = orderBy(
items,
sort.field,
sort.direction
).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE);
const { requestId, items, totalItems } = data;
const groupIds = JSON.stringify(
currentPageErrorGroups.map(({ group_id: groupId }) => groupId).sort()
);
const {
data: errorGroupComparisonStatistics,
data: errorGroupComparisonStatistics = INITIAL_STATE_COMPARISON_STATISTICS,
status: errorGroupComparisonStatisticsStatus,
} = useFetcher(
(callApmApi) => {
if (
requestId &&
currentPageErrorGroups.length &&
start &&
end &&
transactionType
) {
if (requestId && items.length && start && end && transactionType) {
return callApmApi({
endpoint:
'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics',
@ -121,21 +155,26 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
end,
numBuckets: 20,
transactionType,
groupIds,
groupIds: JSON.stringify(
items.map(({ group_id: groupId }) => groupId).sort()
),
comparisonStart,
comparisonEnd,
},
},
});
}
},
// only fetches agg results when requestId or group ids change
// only fetches agg results when requestId changes
// eslint-disable-next-line react-hooks/exhaustive-deps
[requestId, groupIds],
[requestId],
{ preservePreviousData: false }
);
const columns = getColumns({
serviceName,
errorGroupComparisonStatistics: errorGroupComparisonStatistics ?? {},
errorGroupComparisonStatistics,
comparisonEnabled,
});
return (
@ -164,16 +203,16 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
<TableFetchWrapper status={status}>
<ServiceOverviewTableContainer
isEmptyAndLoading={
items.length === 0 && status === FETCH_STATUS.LOADING
totalItems === 0 && status === FETCH_STATUS.LOADING
}
>
<EuiBasicTable
columns={columns}
items={currentPageErrorGroups}
items={items}
pagination={{
pageIndex,
pageSize: PAGE_SIZE,
totalItemCount: items.length,
totalItemCount: totalItems,
pageSizeOptions: [PAGE_SIZE],
hidePerPageOptions: true,
}}

View file

@ -10,11 +10,15 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { useParams } from 'react-router-dom';
import { asTransactionRate } from '../../../../common/utils/formatters';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useFetcher } from '../../../hooks/use_fetcher';
import { useTheme } from '../../../hooks/use_theme';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { TimeseriesChart } from '../../shared/charts/timeseries_chart';
import {
getComparisonChartTheme,
getTimeRangeComparison,
} from '../../shared/time_comparison/get_time_range_comparison';
const INITIAL_STATE = {
currentPeriod: [],
@ -29,9 +33,22 @@ export function ServiceOverviewThroughputChart({
const theme = useTheme();
const { serviceName } = useParams<{ serviceName?: string }>();
const {
urlParams: { environment, kuery, start, end },
urlParams: {
environment,
kuery,
start,
end,
comparisonEnabled,
comparisonType,
},
} = useUrlParams();
const { transactionType } = useApmServiceContext();
const comparisonChartTheme = getComparisonChartTheme(theme);
const { comparisonStart, comparisonEnd } = getTimeRangeComparison({
start,
end,
comparisonType,
});
const { data = INITIAL_STATE, status } = useFetcher(
(callApmApi) => {
@ -48,14 +65,49 @@ export function ServiceOverviewThroughputChart({
start,
end,
transactionType,
comparisonStart,
comparisonEnd,
},
},
});
}
},
[environment, kuery, serviceName, start, end, transactionType]
[
environment,
kuery,
serviceName,
start,
end,
transactionType,
comparisonStart,
comparisonEnd,
]
);
const timeseries = [
{
data: data.currentPeriod,
type: 'linemark',
color: theme.eui.euiColorVis0,
title: i18n.translate('xpack.apm.serviceOverview.throughtputChartTitle', {
defaultMessage: 'Throughput',
}),
},
...(comparisonEnabled
? [
{
data: data.previousPeriod,
type: 'area',
color: theme.eui.euiColorLightestShade,
title: i18n.translate(
'xpack.apm.serviceOverview.throughtputChart.previousPeriodLabel',
{ defaultMessage: 'Previous period' }
),
},
]
: []),
];
return (
<EuiPanel>
<EuiTitle size="xs">
@ -70,18 +122,9 @@ export function ServiceOverviewThroughputChart({
height={height}
showAnnotations={false}
fetchStatus={status}
timeseries={[
{
data: data.currentPeriod,
type: 'linemark',
color: theme.eui.euiColorVis0,
title: i18n.translate(
'xpack.apm.serviceOverview.throughtputChartTitle',
{ defaultMessage: 'Throughput' }
),
},
]}
timeseries={timeseries}
yLabelFormat={asTransactionRate}
customTheme={comparisonChartTheme}
/>
</EuiPanel>
);

View file

@ -159,14 +159,13 @@ export function getColumns({
transactionGroupComparisonStatistics?.currentPeriod?.[name]?.impact ??
0;
const previousImpact =
transactionGroupComparisonStatistics?.previousPeriod?.[name]
?.impact ?? 0;
transactionGroupComparisonStatistics?.previousPeriod?.[name]?.impact;
return (
<EuiFlexGroup gutterSize="xs" direction="column">
<EuiFlexItem>
<ImpactBar value={currentImpact} size="m" />
</EuiFlexItem>
{comparisonEnabled && (
{comparisonEnabled && previousImpact && (
<EuiFlexItem>
<ImpactBar value={previousImpact} size="s" color="subdued" />
</EuiFlexItem>

View file

@ -9,12 +9,17 @@ import { EuiPanel, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useParams } from 'react-router-dom';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { asPercent } from '../../../../../common/utils/formatters';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { useTheme } from '../../../../hooks/use_theme';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { TimeseriesChart } from '../timeseries_chart';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import {
getComparisonChartTheme,
getTimeRangeComparison,
} from '../../time_comparison/get_time_range_comparison';
function yLabelFormat(y?: number | null) {
return asPercent(y || 0, 1);
@ -25,6 +30,21 @@ interface Props {
showAnnotations?: boolean;
}
type ErrorRate = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/error_rate'>;
const INITIAL_STATE: ErrorRate = {
currentPeriod: {
noHits: true,
transactionErrorRate: [],
average: null,
},
previousPeriod: {
noHits: true,
transactionErrorRate: [],
average: null,
},
};
export function TransactionErrorRateChart({
height,
showAnnotations = true,
@ -32,11 +52,25 @@ export function TransactionErrorRateChart({
const theme = useTheme();
const { serviceName } = useParams<{ serviceName?: string }>();
const {
urlParams: { environment, kuery, start, end, transactionName },
urlParams: {
environment,
kuery,
start,
end,
transactionName,
comparisonEnabled,
comparisonType,
},
} = useUrlParams();
const { transactionType } = useApmServiceContext();
const comparisonChartThem = getComparisonChartTheme(theme);
const { comparisonStart, comparisonEnd } = getTimeRangeComparison({
start,
end,
comparisonType,
});
const { data, status } = useFetcher(
const { data = INITIAL_STATE, status } = useFetcher(
(callApmApi) => {
if (transactionType && serviceName && start && end) {
return callApmApi({
@ -53,6 +87,8 @@ export function TransactionErrorRateChart({
end,
transactionType,
transactionName,
comparisonStart,
comparisonEnd,
},
},
});
@ -66,10 +102,34 @@ export function TransactionErrorRateChart({
end,
transactionType,
transactionName,
comparisonStart,
comparisonEnd,
]
);
const errorRates = data?.transactionErrorRate || [];
const timeseries = [
{
data: data.currentPeriod.transactionErrorRate,
type: 'linemark',
color: theme.eui.euiColorVis7,
title: i18n.translate('xpack.apm.errorRate.chart.errorRate', {
defaultMessage: 'Error rate (avg.)',
}),
},
...(comparisonEnabled
? [
{
data: data.previousPeriod.transactionErrorRate,
type: 'area',
color: theme.eui.euiColorLightestShade,
title: i18n.translate(
'xpack.apm.errorRate.chart.errorRate.previousPeriodLabel',
{ defaultMessage: 'Previous period' }
),
},
]
: []),
];
return (
<EuiPanel>
@ -85,18 +145,10 @@ export function TransactionErrorRateChart({
height={height}
showAnnotations={showAnnotations}
fetchStatus={status}
timeseries={[
{
data: errorRates,
type: 'linemark',
color: theme.eui.euiColorVis7,
title: i18n.translate('xpack.apm.errorRate.chart.errorRate', {
defaultMessage: 'Error rate (avg.)',
}),
},
]}
timeseries={timeseries}
yLabelFormat={yLabelFormat}
yDomain={{ min: 0, max: 1 }}
customTheme={comparisonChartThem}
/>
</EuiPanel>
);

View file

@ -106,11 +106,14 @@ async function getErrorStats({
searchAggregatedTransactions: boolean;
}) {
return withApmSpan('get_error_rate_for_service_map_node', async () => {
const { start, end } = setup;
const { noHits, average } = await getErrorRate({
environment,
setup,
serviceName,
searchAggregatedTransactions,
start,
end,
});
return { avgErrorRate: noHits ? null : average };

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { keyBy } from 'lodash';
import { Coordinate } from '../../../../typings/timeseries';
import {
ERROR_GROUP_ID,
SERVICE_NAME,
@ -16,6 +17,7 @@ import {
rangeQuery,
kqlQuery,
} from '../../../../server/utils/queries';
import { offsetPreviousPeriodCoordinates } from '../../../utils/offset_previous_period_coordinate';
import { withApmSpan } from '../../../utils/with_apm_span';
import { getBucketSize } from '../../helpers/get_bucket_size';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
@ -28,19 +30,23 @@ export async function getServiceErrorGroupComparisonStatistics({
transactionType,
groupIds,
environment,
start,
end,
}: {
kuery?: string;
serviceName: string;
setup: Setup & SetupTimeRange;
setup: Setup;
numBuckets: number;
transactionType: string;
groupIds: string[];
environment?: string;
}) {
start: number;
end: number;
}): Promise<Array<{ groupId: string; timeseries: Coordinate[] }>> {
return withApmSpan(
'get_service_error_group_comparison_statistics',
async () => {
const { apmEventClient, start, end } = setup;
const { apmEventClient } = setup;
const { intervalString } = getBucketSize({ start, end, numBuckets });
@ -87,10 +93,10 @@ export async function getServiceErrorGroupComparisonStatistics({
});
if (!timeseriesResponse.aggregations) {
return {};
return [];
}
const groups = timeseriesResponse.aggregations.error_groups.buckets.map(
return timeseriesResponse.aggregations.error_groups.buckets.map(
(bucket) => {
const groupId = bucket.key as string;
return {
@ -104,8 +110,76 @@ export async function getServiceErrorGroupComparisonStatistics({
};
}
);
return keyBy(groups, 'groupId');
}
);
}
export async function getServiceErrorGroupPeriods({
kuery,
serviceName,
setup,
numBuckets,
transactionType,
groupIds,
environment,
comparisonStart,
comparisonEnd,
}: {
kuery?: string;
serviceName: string;
setup: Setup & SetupTimeRange;
numBuckets: number;
transactionType: string;
groupIds: string[];
environment?: string;
comparisonStart?: number;
comparisonEnd?: number;
}) {
const { start, end } = setup;
const commonProps = {
environment,
kuery,
serviceName,
setup,
numBuckets,
transactionType,
groupIds,
};
const currentPeriodPromise = getServiceErrorGroupComparisonStatistics({
...commonProps,
start,
end,
});
const previousPeriodPromise =
comparisonStart && comparisonEnd
? getServiceErrorGroupComparisonStatistics({
...commonProps,
start: comparisonStart,
end: comparisonEnd,
})
: [];
const [currentPeriod, previousPeriod] = await Promise.all([
currentPeriodPromise,
previousPeriodPromise,
]);
const firtCurrentPeriod = currentPeriod.length ? currentPeriod[0] : undefined;
return {
currentPeriod: keyBy(currentPeriod, 'groupId'),
previousPeriod: keyBy(
previousPeriod.map((errorRateGroup) => ({
...errorRateGroup,
timeseries: offsetPreviousPeriodCoordinates({
currentPeriodTimeseries: firtCurrentPeriod?.timeseries,
previousPeriodTimeseries: errorRateGroup.timeseries,
}),
})),
'groupId'
),
};
}

View file

@ -31,6 +31,7 @@ import {
getTransactionErrorRateTimeSeries,
} from '../helpers/transaction_error_rate';
import { withApmSpan } from '../../utils/with_apm_span';
import { offsetPreviousPeriodCoordinates } from '../../utils/offset_previous_period_coordinate';
export async function getErrorRate({
environment,
@ -40,21 +41,25 @@ export async function getErrorRate({
transactionName,
setup,
searchAggregatedTransactions,
start,
end,
}: {
environment?: string;
kuery?: string;
serviceName: string;
transactionType?: string;
transactionName?: string;
setup: Setup & SetupTimeRange;
setup: Setup;
searchAggregatedTransactions: boolean;
start: number;
end: number;
}): Promise<{
noHits: boolean;
transactionErrorRate: Coordinate[];
average: number | null;
}> {
return withApmSpan('get_transaction_group_error_rate', async () => {
const { start, end, apmEventClient } = setup;
const { apmEventClient } = setup;
const transactionNamefilter = transactionName
? [{ term: { [TRANSACTION_NAME]: transactionName } }]
@ -129,3 +134,67 @@ export async function getErrorRate({
return { noHits, transactionErrorRate, average };
});
}
export async function getErrorRatePeriods({
environment,
kuery,
serviceName,
transactionType,
transactionName,
setup,
searchAggregatedTransactions,
comparisonStart,
comparisonEnd,
}: {
environment?: string;
kuery?: string;
serviceName: string;
transactionType?: string;
transactionName?: string;
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
comparisonStart?: number;
comparisonEnd?: number;
}) {
const { start, end } = setup;
const commonProps = {
environment,
kuery,
serviceName,
transactionType,
transactionName,
setup,
searchAggregatedTransactions,
};
const currentPeriodPromise = getErrorRate({ ...commonProps, start, end });
const previousPeriodPromise =
comparisonStart && comparisonEnd
? getErrorRate({
...commonProps,
start: comparisonStart,
end: comparisonEnd,
})
: { noHits: true, transactionErrorRate: [], average: null };
const [currentPeriod, previousPeriod] = await Promise.all([
currentPeriodPromise,
previousPeriodPromise,
]);
const firtCurrentPeriod = currentPeriod.transactionErrorRate.length
? currentPeriod.transactionErrorRate
: undefined;
return {
currentPeriod,
previousPeriod: {
...previousPeriod,
transactionErrorRate: offsetPreviousPeriodCoordinates({
currentPeriodTimeseries: firtCurrentPeriod,
previousPeriodTimeseries: previousPeriod.transactionErrorRate,
}),
},
};
}

View file

@ -17,7 +17,7 @@ import { getServices } from '../lib/services/get_services';
import { getServiceAgentName } from '../lib/services/get_service_agent_name';
import { getServiceDependencies } from '../lib/services/get_service_dependencies';
import { getServiceErrorGroupPrimaryStatistics } from '../lib/services/get_service_error_groups/get_service_error_group_primary_statistics';
import { getServiceErrorGroupComparisonStatistics } from '../lib/services/get_service_error_groups/get_service_error_group_comparison_statistics';
import { getServiceErrorGroupPeriods } from '../lib/services/get_service_error_groups/get_service_error_group_comparison_statistics';
import { getServiceInstances } from '../lib/services/get_service_instances';
import { getServiceMetadataDetails } from '../lib/services/get_service_metadata_details';
import { getServiceMetadataIcons } from '../lib/services/get_service_metadata_icons';
@ -329,6 +329,7 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
environmentRt,
kueryRt,
rangeRt,
comparisonRangeRt,
t.type({
numBuckets: toNumberRt,
transactionType: t.string,
@ -342,10 +343,18 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
const {
path: { serviceName },
query: { environment, kuery, numBuckets, transactionType, groupIds },
query: {
environment,
kuery,
numBuckets,
transactionType,
groupIds,
comparisonStart,
comparisonEnd,
},
} = context.params;
return getServiceErrorGroupComparisonStatistics({
return getServiceErrorGroupPeriods({
environment,
kuery,
serviceName,
@ -353,6 +362,8 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
numBuckets,
transactionType,
groupIds,
comparisonStart,
comparisonEnd,
});
},
});

View file

@ -22,7 +22,7 @@ import { getAnomalySeries } from '../lib/transactions/get_anomaly_data';
import { getLatencyPeriods } from '../lib/transactions/get_latency_charts';
import { getThroughputCharts } from '../lib/transactions/get_throughput_charts';
import { getTransactionGroupList } from '../lib/transaction_groups';
import { getErrorRate } from '../lib/transaction_groups/get_error_rate';
import { getErrorRatePeriods } from '../lib/transaction_groups/get_error_rate';
import { createRoute } from './create_route';
import {
comparisonRangeRt,
@ -380,11 +380,9 @@ export const transactionChartsErrorRateRoute = createRoute({
serviceName: t.string,
}),
query: t.intersection([
environmentRt,
kueryRt,
rangeRt,
t.type({ transactionType: t.string }),
t.partial({ transactionName: t.string }),
t.intersection([environmentRt, kueryRt, rangeRt, comparisonRangeRt]),
]),
}),
options: { tags: ['access:apm'] },
@ -397,13 +395,15 @@ export const transactionChartsErrorRateRoute = createRoute({
kuery,
transactionType,
transactionName,
comparisonStart,
comparisonEnd,
} = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
return getErrorRate({
return getErrorRatePeriods({
environment,
kuery,
serviceName,
@ -411,6 +411,8 @@ export const transactionChartsErrorRateRoute = createRoute({
transactionName,
setup,
searchAggregatedTransactions,
comparisonStart,
comparisonEnd,
});
},
});

View file

@ -131,3 +131,75 @@ Object {
],
}
`;
exports[`APM API tests basic apm_8.0.0 Error groups comparison statistics when data is loaded with previous data returns the correct data returns correct timeseries 1`] = `
Object {
"groupId": "051f95eabf120ebe2f8b0399fe3e54c5",
"timeseries": Array [
Object {
"x": 1607436720000,
"y": 0,
},
Object {
"x": 1607436780000,
"y": 0,
},
Object {
"x": 1607436840000,
"y": 0,
},
Object {
"x": 1607436900000,
"y": 0,
},
Object {
"x": 1607436960000,
"y": 0,
},
Object {
"x": 1607437020000,
"y": 0,
},
Object {
"x": 1607437080000,
"y": 0,
},
Object {
"x": 1607437140000,
"y": 0,
},
Object {
"x": 1607437200000,
"y": 2,
},
Object {
"x": 1607437260000,
"y": 0,
},
Object {
"x": 1607437320000,
"y": 1,
},
Object {
"x": 1607437380000,
"y": 0,
},
Object {
"x": 1607437440000,
"y": 0,
},
Object {
"x": 1607437500000,
"y": 0,
},
Object {
"x": 1607437560000,
"y": 0,
},
Object {
"x": 1607437620000,
"y": 0,
},
],
}
`;

View file

@ -7,6 +7,7 @@
import url from 'url';
import expect from '@kbn/expect';
import moment from 'moment';
import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { registry } from '../../common/registry';
@ -45,8 +46,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
},
})
);
expect(response.status).to.be(200);
expect(response.body).to.empty();
expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} });
});
}
);
@ -72,20 +74,23 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(response.status).to.be(200);
const errorGroupsComparisonStatistics = response.body as ErrorGroupsComparisonStatistics;
expect(Object.keys(errorGroupsComparisonStatistics).sort()).to.eql(groupIds.sort());
expect(Object.keys(errorGroupsComparisonStatistics.currentPeriod).sort()).to.eql(
groupIds.sort()
);
groupIds.forEach((groupId) => {
expect(errorGroupsComparisonStatistics[groupId]).not.to.be.empty();
expect(errorGroupsComparisonStatistics.currentPeriod[groupId]).not.to.be.empty();
});
const errorgroupsComparisonStatistics = errorGroupsComparisonStatistics[groupIds[0]];
const errorgroupsComparisonStatistics =
errorGroupsComparisonStatistics.currentPeriod[groupIds[0]];
expect(
errorgroupsComparisonStatistics.timeseries.map(({ y }) => isFinite(y)).length
errorgroupsComparisonStatistics.timeseries.map(({ y }) => y && isFinite(y)).length
).to.be.greaterThan(0);
expectSnapshot(errorgroupsComparisonStatistics).toMatch();
});
it('returns an empty list when requested groupIds are not available in the given time range', async () => {
it('returns an empty state when requested groupIds are not available in the given time range', async () => {
const response = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`,
@ -100,7 +105,82 @@ export default function ApiTest({ getService }: FtrProviderContext) {
);
expect(response.status).to.be(200);
expect(response.body).to.empty();
expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} });
});
}
);
registry.when(
'Error groups comparison statistics when data is loaded with previous data',
{ config: 'basic', archives: [archiveName] },
() => {
describe('returns the correct data', async () => {
let response: {
status: number;
body: ErrorGroupsComparisonStatistics;
};
before(async () => {
response = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`,
query: {
numBuckets: 20,
transactionType: 'request',
groupIds: JSON.stringify(groupIds),
start: moment(end).subtract(15, 'minutes').toISOString(),
end,
comparisonStart: start,
comparisonEnd: moment(start).add(15, 'minutes').toISOString(),
},
})
);
expect(response.status).to.be(200);
});
it('returns correct timeseries', () => {
const errorGroupsComparisonStatistics = response.body as ErrorGroupsComparisonStatistics;
const errorgroupsComparisonStatistics =
errorGroupsComparisonStatistics.currentPeriod[groupIds[0]];
expect(
errorgroupsComparisonStatistics.timeseries.map(({ y }) => y && isFinite(y)).length
).to.be.greaterThan(0);
expectSnapshot(errorgroupsComparisonStatistics).toMatch();
});
it('matches x-axis on current period and previous period', () => {
const errorGroupsComparisonStatistics = response.body as ErrorGroupsComparisonStatistics;
const currentPeriodItems = Object.values(errorGroupsComparisonStatistics.currentPeriod);
const previousPeriodItems = Object.values(errorGroupsComparisonStatistics.previousPeriod);
const currentPeriodFirstItem = currentPeriodItems[0];
const previousPeriodFirstItem = previousPeriodItems[0];
expect(currentPeriodFirstItem.timeseries.map(({ x }) => x)).to.be.eql(
previousPeriodFirstItem.timeseries.map(({ x }) => x)
);
});
});
it('returns an empty state when requested groupIds are not available in the given time range', async () => {
const response = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`,
query: {
numBuckets: 20,
transactionType: 'request',
groupIds: JSON.stringify(['foo']),
start: moment(end).subtract(15, 'minutes').toISOString(),
end,
comparisonStart: start,
comparisonEnd: moment(start).add(15, 'minutes').toISOString(),
},
})
);
expect(response.status).to.be(200);
expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} });
});
}
);

View file

@ -248,3 +248,741 @@ Array [
},
]
`;
exports[`APM API tests basic apm_8.0.0 Error rate when data is loaded returns the transaction error rate with comparison data has the correct error rate 1`] = `
Array [
Object {
"x": 1607436770000,
"y": null,
},
Object {
"x": 1607436780000,
"y": null,
},
Object {
"x": 1607436790000,
"y": null,
},
Object {
"x": 1607436800000,
"y": null,
},
Object {
"x": 1607436810000,
"y": null,
},
Object {
"x": 1607436820000,
"y": 0,
},
Object {
"x": 1607436830000,
"y": null,
},
Object {
"x": 1607436840000,
"y": null,
},
Object {
"x": 1607436850000,
"y": null,
},
Object {
"x": 1607436860000,
"y": 1,
},
Object {
"x": 1607436870000,
"y": 0,
},
Object {
"x": 1607436880000,
"y": 0,
},
Object {
"x": 1607436890000,
"y": null,
},
Object {
"x": 1607436900000,
"y": null,
},
Object {
"x": 1607436910000,
"y": null,
},
Object {
"x": 1607436920000,
"y": null,
},
Object {
"x": 1607436930000,
"y": null,
},
Object {
"x": 1607436940000,
"y": null,
},
Object {
"x": 1607436950000,
"y": null,
},
Object {
"x": 1607436960000,
"y": null,
},
Object {
"x": 1607436970000,
"y": null,
},
Object {
"x": 1607436980000,
"y": 0,
},
Object {
"x": 1607436990000,
"y": 0,
},
Object {
"x": 1607437000000,
"y": 0,
},
Object {
"x": 1607437010000,
"y": null,
},
Object {
"x": 1607437020000,
"y": null,
},
Object {
"x": 1607437030000,
"y": null,
},
Object {
"x": 1607437040000,
"y": null,
},
Object {
"x": 1607437050000,
"y": null,
},
Object {
"x": 1607437060000,
"y": null,
},
Object {
"x": 1607437070000,
"y": null,
},
Object {
"x": 1607437080000,
"y": null,
},
Object {
"x": 1607437090000,
"y": null,
},
Object {
"x": 1607437100000,
"y": 0,
},
Object {
"x": 1607437110000,
"y": 0,
},
Object {
"x": 1607437120000,
"y": null,
},
Object {
"x": 1607437130000,
"y": null,
},
Object {
"x": 1607437140000,
"y": null,
},
Object {
"x": 1607437150000,
"y": null,
},
Object {
"x": 1607437160000,
"y": null,
},
Object {
"x": 1607437170000,
"y": null,
},
Object {
"x": 1607437180000,
"y": null,
},
Object {
"x": 1607437190000,
"y": null,
},
Object {
"x": 1607437200000,
"y": null,
},
Object {
"x": 1607437210000,
"y": null,
},
Object {
"x": 1607437220000,
"y": 0,
},
Object {
"x": 1607437230000,
"y": 0.6,
},
Object {
"x": 1607437240000,
"y": 0,
},
Object {
"x": 1607437250000,
"y": null,
},
Object {
"x": 1607437260000,
"y": null,
},
Object {
"x": 1607437270000,
"y": 0,
},
Object {
"x": 1607437280000,
"y": null,
},
Object {
"x": 1607437290000,
"y": null,
},
Object {
"x": 1607437300000,
"y": null,
},
Object {
"x": 1607437310000,
"y": null,
},
Object {
"x": 1607437320000,
"y": null,
},
Object {
"x": 1607437330000,
"y": null,
},
Object {
"x": 1607437340000,
"y": 0,
},
Object {
"x": 1607437350000,
"y": null,
},
Object {
"x": 1607437360000,
"y": 0.5,
},
Object {
"x": 1607437370000,
"y": null,
},
Object {
"x": 1607437380000,
"y": null,
},
Object {
"x": 1607437390000,
"y": null,
},
Object {
"x": 1607437400000,
"y": null,
},
Object {
"x": 1607437410000,
"y": null,
},
Object {
"x": 1607437420000,
"y": null,
},
Object {
"x": 1607437430000,
"y": null,
},
Object {
"x": 1607437440000,
"y": null,
},
Object {
"x": 1607437450000,
"y": null,
},
Object {
"x": 1607437460000,
"y": 1,
},
Object {
"x": 1607437470000,
"y": 0,
},
Object {
"x": 1607437480000,
"y": 1,
},
Object {
"x": 1607437490000,
"y": null,
},
Object {
"x": 1607437500000,
"y": null,
},
Object {
"x": 1607437510000,
"y": null,
},
Object {
"x": 1607437520000,
"y": null,
},
Object {
"x": 1607437530000,
"y": null,
},
Object {
"x": 1607437540000,
"y": null,
},
Object {
"x": 1607437550000,
"y": null,
},
Object {
"x": 1607437560000,
"y": null,
},
Object {
"x": 1607437570000,
"y": 0,
},
Object {
"x": 1607437580000,
"y": null,
},
Object {
"x": 1607437590000,
"y": null,
},
Object {
"x": 1607437600000,
"y": null,
},
Object {
"x": 1607437610000,
"y": null,
},
Object {
"x": 1607437620000,
"y": null,
},
Object {
"x": 1607437630000,
"y": null,
},
Object {
"x": 1607437640000,
"y": null,
},
Object {
"x": 1607437650000,
"y": null,
},
Object {
"x": 1607437660000,
"y": null,
},
Object {
"x": 1607437670000,
"y": null,
},
]
`;
exports[`APM API tests basic apm_8.0.0 Error rate when data is loaded returns the transaction error rate with comparison data has the correct error rate 2`] = `
Array [
Object {
"x": 1607436770000,
"y": null,
},
Object {
"x": 1607436780000,
"y": null,
},
Object {
"x": 1607436790000,
"y": null,
},
Object {
"x": 1607436800000,
"y": 0,
},
Object {
"x": 1607436810000,
"y": null,
},
Object {
"x": 1607436820000,
"y": null,
},
Object {
"x": 1607436830000,
"y": null,
},
Object {
"x": 1607436840000,
"y": 0,
},
Object {
"x": 1607436850000,
"y": null,
},
Object {
"x": 1607436860000,
"y": null,
},
Object {
"x": 1607436870000,
"y": null,
},
Object {
"x": 1607436880000,
"y": null,
},
Object {
"x": 1607436890000,
"y": null,
},
Object {
"x": 1607436900000,
"y": null,
},
Object {
"x": 1607436910000,
"y": 0,
},
Object {
"x": 1607436920000,
"y": 0,
},
Object {
"x": 1607436930000,
"y": 0,
},
Object {
"x": 1607436940000,
"y": null,
},
Object {
"x": 1607436950000,
"y": null,
},
Object {
"x": 1607436960000,
"y": null,
},
Object {
"x": 1607436970000,
"y": null,
},
Object {
"x": 1607436980000,
"y": null,
},
Object {
"x": 1607436990000,
"y": null,
},
Object {
"x": 1607437000000,
"y": null,
},
Object {
"x": 1607437010000,
"y": null,
},
Object {
"x": 1607437020000,
"y": null,
},
Object {
"x": 1607437030000,
"y": 0,
},
Object {
"x": 1607437040000,
"y": 0,
},
Object {
"x": 1607437050000,
"y": null,
},
Object {
"x": 1607437060000,
"y": null,
},
Object {
"x": 1607437070000,
"y": null,
},
Object {
"x": 1607437080000,
"y": null,
},
Object {
"x": 1607437090000,
"y": null,
},
Object {
"x": 1607437100000,
"y": null,
},
Object {
"x": 1607437110000,
"y": null,
},
Object {
"x": 1607437120000,
"y": null,
},
Object {
"x": 1607437130000,
"y": null,
},
Object {
"x": 1607437140000,
"y": null,
},
Object {
"x": 1607437150000,
"y": null,
},
Object {
"x": 1607437160000,
"y": 0,
},
Object {
"x": 1607437170000,
"y": null,
},
Object {
"x": 1607437180000,
"y": null,
},
Object {
"x": 1607437190000,
"y": null,
},
Object {
"x": 1607437200000,
"y": null,
},
Object {
"x": 1607437210000,
"y": null,
},
Object {
"x": 1607437220000,
"y": null,
},
Object {
"x": 1607437230000,
"y": null,
},
Object {
"x": 1607437240000,
"y": 1,
},
Object {
"x": 1607437250000,
"y": null,
},
Object {
"x": 1607437260000,
"y": null,
},
Object {
"x": 1607437270000,
"y": null,
},
Object {
"x": 1607437280000,
"y": 0,
},
Object {
"x": 1607437290000,
"y": 0,
},
Object {
"x": 1607437300000,
"y": null,
},
Object {
"x": 1607437310000,
"y": null,
},
Object {
"x": 1607437320000,
"y": null,
},
Object {
"x": 1607437330000,
"y": null,
},
Object {
"x": 1607437340000,
"y": null,
},
Object {
"x": 1607437350000,
"y": null,
},
Object {
"x": 1607437360000,
"y": null,
},
Object {
"x": 1607437370000,
"y": null,
},
Object {
"x": 1607437380000,
"y": null,
},
Object {
"x": 1607437390000,
"y": null,
},
Object {
"x": 1607437400000,
"y": 0,
},
Object {
"x": 1607437410000,
"y": 0,
},
Object {
"x": 1607437420000,
"y": 0.25,
},
Object {
"x": 1607437430000,
"y": null,
},
Object {
"x": 1607437440000,
"y": null,
},
Object {
"x": 1607437450000,
"y": null,
},
Object {
"x": 1607437460000,
"y": null,
},
Object {
"x": 1607437470000,
"y": null,
},
Object {
"x": 1607437480000,
"y": null,
},
Object {
"x": 1607437490000,
"y": null,
},
Object {
"x": 1607437500000,
"y": null,
},
Object {
"x": 1607437510000,
"y": null,
},
Object {
"x": 1607437520000,
"y": 0.5,
},
Object {
"x": 1607437530000,
"y": 0.2,
},
Object {
"x": 1607437540000,
"y": 0,
},
Object {
"x": 1607437550000,
"y": null,
},
Object {
"x": 1607437560000,
"y": null,
},
Object {
"x": 1607437570000,
"y": null,
},
Object {
"x": 1607437580000,
"y": null,
},
Object {
"x": 1607437590000,
"y": null,
},
Object {
"x": 1607437600000,
"y": null,
},
Object {
"x": 1607437610000,
"y": null,
},
Object {
"x": 1607437620000,
"y": null,
},
Object {
"x": 1607437630000,
"y": null,
},
Object {
"x": 1607437640000,
"y": null,
},
Object {
"x": 1607437650000,
"y": 1,
},
Object {
"x": 1607437660000,
"y": 0,
},
Object {
"x": 1607437670000,
"y": null,
},
]
`;

View file

@ -8,10 +8,14 @@
import expect from '@kbn/expect';
import { first, last } from 'lodash';
import { format } from 'url';
import moment from 'moment';
import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi';
import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { registry } from '../../common/registry';
type ErrorRate = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/error_rate'>;
export default function ApiTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
@ -21,20 +25,43 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const { start, end } = archives_metadata[archiveName];
const transactionType = 'request';
const url = format({
pathname: '/api/apm/services/opbeans-java/transactions/charts/error_rate',
query: { start, end, transactionType },
});
registry.when('Error rate when data is not loaded', { config: 'basic', archives: [] }, () => {
it('handles the empty state', async () => {
const response = await supertest.get(url);
const response = await supertest.get(
format({
pathname: '/api/apm/services/opbeans-java/transactions/charts/error_rate',
query: { start, end, transactionType },
})
);
expect(response.status).to.be(200);
expect(response.body.noHits).to.be(true);
const body = response.body as ErrorRate;
expect(body).to.be.eql({
currentPeriod: { noHits: true, transactionErrorRate: [], average: null },
previousPeriod: { noHits: true, transactionErrorRate: [], average: null },
});
});
expect(response.body.transactionErrorRate.length).to.be(0);
expect(response.body.average).to.be(null);
it('handles the empty state with comparison data', async () => {
const response = await supertest.get(
format({
pathname: '/api/apm/services/opbeans-java/transactions/charts/error_rate',
query: {
transactionType,
start: moment(end).subtract(15, 'minutes').toISOString(),
end,
comparisonStart: start,
comparisonEnd: moment(start).add(15, 'minutes').toISOString(),
},
})
);
expect(response.status).to.be(200);
const body = response.body as ErrorRate;
expect(body).to.be.eql({
currentPeriod: { noHits: true, transactionErrorRate: [], average: null },
previousPeriod: { noHits: true, transactionErrorRate: [], average: null },
});
});
});
@ -43,22 +70,26 @@ export default function ApiTest({ getService }: FtrProviderContext) {
{ config: 'basic', archives: [archiveName] },
() => {
describe('returns the transaction error rate', () => {
let errorRateResponse: {
transactionErrorRate: Array<{ x: number; y: number | null }>;
average: number;
};
let errorRateResponse: ErrorRate;
before(async () => {
const response = await supertest.get(url);
const response = await supertest.get(
format({
pathname: '/api/apm/services/opbeans-java/transactions/charts/error_rate',
query: { start, end, transactionType },
})
);
errorRateResponse = response.body;
});
it('returns some data', () => {
expect(errorRateResponse.average).to.be.greaterThan(0);
expect(errorRateResponse.currentPeriod.average).to.be.greaterThan(0);
expect(errorRateResponse.previousPeriod.average).to.be(null);
expect(errorRateResponse.transactionErrorRate.length).to.be.greaterThan(0);
expect(errorRateResponse.currentPeriod.transactionErrorRate.length).to.be.greaterThan(0);
expect(errorRateResponse.previousPeriod.transactionErrorRate).to.empty();
const nonNullDataPoints = errorRateResponse.transactionErrorRate.filter(
const nonNullDataPoints = errorRateResponse.currentPeriod.transactionErrorRate.filter(
({ y }) => y !== null
);
@ -67,26 +98,126 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('has the correct start date', () => {
expectSnapshot(
new Date(first(errorRateResponse.transactionErrorRate)?.x ?? NaN).toISOString()
new Date(
first(errorRateResponse.currentPeriod.transactionErrorRate)?.x ?? NaN
).toISOString()
).toMatchInline(`"2020-12-08T13:57:30.000Z"`);
});
it('has the correct end date', () => {
expectSnapshot(
new Date(last(errorRateResponse.transactionErrorRate)?.x ?? NaN).toISOString()
new Date(
last(errorRateResponse.currentPeriod.transactionErrorRate)?.x ?? NaN
).toISOString()
).toMatchInline(`"2020-12-08T14:27:30.000Z"`);
});
it('has the correct number of buckets', () => {
expectSnapshot(errorRateResponse.transactionErrorRate.length).toMatchInline(`61`);
expectSnapshot(errorRateResponse.currentPeriod.transactionErrorRate.length).toMatchInline(
`61`
);
});
it('has the correct calculation for average', () => {
expectSnapshot(errorRateResponse.average).toMatchInline(`0.16`);
expectSnapshot(errorRateResponse.currentPeriod.average).toMatchInline(`0.16`);
});
it('has the correct error rate', () => {
expectSnapshot(errorRateResponse.transactionErrorRate).toMatch();
expectSnapshot(errorRateResponse.currentPeriod.transactionErrorRate).toMatch();
});
});
describe('returns the transaction error rate with comparison data', () => {
let errorRateResponse: ErrorRate;
before(async () => {
const response = await supertest.get(
format({
pathname: '/api/apm/services/opbeans-java/transactions/charts/error_rate',
query: {
transactionType,
start: moment(end).subtract(15, 'minutes').toISOString(),
end,
comparisonStart: start,
comparisonEnd: moment(start).add(15, 'minutes').toISOString(),
},
})
);
errorRateResponse = response.body;
});
it('returns some data', () => {
expect(errorRateResponse.currentPeriod.average).to.be.greaterThan(0);
expect(errorRateResponse.previousPeriod.average).to.be.greaterThan(0);
expect(errorRateResponse.currentPeriod.transactionErrorRate.length).to.be.greaterThan(0);
expect(errorRateResponse.previousPeriod.transactionErrorRate.length).to.be.greaterThan(0);
const currentPeriodNonNullDataPoints = errorRateResponse.currentPeriod.transactionErrorRate.filter(
({ y }) => y !== null
);
const previousPeriodNonNullDataPoints = errorRateResponse.previousPeriod.transactionErrorRate.filter(
({ y }) => y !== null
);
expect(currentPeriodNonNullDataPoints.length).to.be.greaterThan(0);
expect(previousPeriodNonNullDataPoints.length).to.be.greaterThan(0);
});
it('has the correct start date', () => {
expectSnapshot(
new Date(
first(errorRateResponse.currentPeriod.transactionErrorRate)?.x ?? NaN
).toISOString()
).toMatchInline(`"2020-12-08T14:12:50.000Z"`);
expectSnapshot(
new Date(
first(errorRateResponse.previousPeriod.transactionErrorRate)?.x ?? NaN
).toISOString()
).toMatchInline(`"2020-12-08T14:12:50.000Z"`);
});
it('has the correct end date', () => {
expectSnapshot(
new Date(
last(errorRateResponse.currentPeriod.transactionErrorRate)?.x ?? NaN
).toISOString()
).toMatchInline(`"2020-12-08T14:27:50.000Z"`);
expectSnapshot(
new Date(
last(errorRateResponse.previousPeriod.transactionErrorRate)?.x ?? NaN
).toISOString()
).toMatchInline(`"2020-12-08T14:27:50.000Z"`);
});
it('has the correct number of buckets', () => {
expectSnapshot(errorRateResponse.currentPeriod.transactionErrorRate.length).toMatchInline(
`91`
);
expectSnapshot(
errorRateResponse.previousPeriod.transactionErrorRate.length
).toMatchInline(`91`);
});
it('has the correct calculation for average', () => {
expectSnapshot(errorRateResponse.currentPeriod.average).toMatchInline(
`0.233333333333333`
);
expectSnapshot(errorRateResponse.previousPeriod.average).toMatchInline(
`0.111111111111111`
);
});
it('has the correct error rate', () => {
expectSnapshot(errorRateResponse.currentPeriod.transactionErrorRate).toMatch();
expectSnapshot(errorRateResponse.previousPeriod.transactionErrorRate).toMatch();
});
it('matches x-axis on current period and previous period', () => {
expect(errorRateResponse.currentPeriod.transactionErrorRate.map(({ x }) => x)).to.be.eql(
errorRateResponse.previousPeriod.transactionErrorRate.map(({ x }) => x)
);
});
});
}