From b0fa077e8a8e75d2937a0d53a0905e0a5d26612a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 16 Mar 2021 09:12:50 -0400 Subject: [PATCH] [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> --- .../get_column.tsx | 16 +- .../service_overview_errors_table/index.tsx | 97 ++- .../service_overview_throughput_chart.tsx | 73 +- .../get_columns.tsx | 5 +- .../transaction_error_rate_chart/index.tsx | 78 +- .../get_service_map_service_node_info.ts | 3 + ...rvice_error_group_comparison_statistics.ts | 88 ++- .../lib/transaction_groups/get_error_rate.ts | 73 +- x-pack/plugins/apm/server/routes/services.ts | 17 +- .../plugins/apm/server/routes/transactions.ts | 12 +- .../error_groups_comparison_statistics.snap | 72 ++ .../error_groups_comparison_statistics.ts | 94 ++- .../__snapshots__/error_rate.snap | 738 ++++++++++++++++++ .../tests/transactions/error_rate.ts | 175 ++++- 14 files changed, 1432 insertions(+), 109 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx index 94913c1678d21..fd1120808db9e 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx @@ -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> { 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 ( ); }, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index bbd36a4c8df93..d36bee8d6be73 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -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 { comparisonStart, comparisonEnd } = getTimeRangeComparison({ + start, + end, + comparisonType, + }); + const { pageIndex, sort } = tableOptions; + const { direction, field } = sort; - const { data = INITIAL_STATE, status } = useFetcher( + 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) { (); 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 ( @@ -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} /> ); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx index bff45b5d274c3..d9ca3356d7fd2 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx @@ -159,14 +159,13 @@ export function getColumns({ transactionGroupComparisonStatistics?.currentPeriod?.[name]?.impact ?? 0; const previousImpact = - transactionGroupComparisonStatistics?.previousPeriod?.[name] - ?.impact ?? 0; + transactionGroupComparisonStatistics?.previousPeriod?.[name]?.impact; return ( - {comparisonEnabled && ( + {comparisonEnabled && previousImpact && ( diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index b3c38651ea178..fd9435db57bfd 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -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 ( @@ -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} /> ); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index a6e7832bf697d..7da3ce772ef5f 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -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 }; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts index e33044bff8ffa..b559f55bbe78e 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts @@ -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> { 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' + ), + }; +} diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 627086df9d681..ec5dd1308cb7e 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -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, + }), + }, + }; +} diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index a84c8dc274248..bac970416792b 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -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, }); }, }); diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index 1571efb373cc9..f3424a252e409 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -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, }); }, }); diff --git a/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap b/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap index a536a6de67ff3..31bc29a2476ca 100644 --- a/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap +++ b/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap @@ -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, + }, + ], +} +`; diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts index 4a19efac5a809..821d0515aa808 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts @@ -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: {} }); }); } ); diff --git a/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/error_rate.snap b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/error_rate.snap index d97d39cda1b8d..7ec68bbc0a9fd 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/error_rate.snap +++ b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/error_rate.snap @@ -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, + }, +] +`; diff --git a/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts b/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts index 2b94816466aa7..ce16ad2c96c3b 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts @@ -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 }, + }); + }); + + 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); - expect(response.body.transactionErrorRate.length).to.be(0); - expect(response.body.average).to.be(null); + 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.currentPeriod.average).toMatchInline(`0.16`); + }); + + it('has the correct error rate', () => { + 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.average).toMatchInline(`0.16`); + expectSnapshot(errorRateResponse.currentPeriod.average).toMatchInline( + `0.233333333333333` + ); + expectSnapshot(errorRateResponse.previousPeriod.average).toMatchInline( + `0.111111111111111` + ); }); it('has the correct error rate', () => { - expectSnapshot(errorRateResponse.transactionErrorRate).toMatch(); + 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) + ); }); }); }