diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index a0ea00ae5c3ad..f6ec2fb24018f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -57,7 +57,11 @@ export function ServiceOverview({ return ( - + 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 b01ad93f2c998..bff45b5d274c3 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiBasicTableColumn } from '@elastic/eui'; +import { EuiBasicTableColumn, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { ValuesType } from 'utility-types'; @@ -34,10 +34,12 @@ export function getColumns({ serviceName, latencyAggregationType, transactionGroupComparisonStatistics, + comparisonEnabled, }: { serviceName: string; latencyAggregationType?: LatencyAggregationType; transactionGroupComparisonStatistics?: TransactionGroupComparisonStatistics; + comparisonEnabled?: boolean; }): Array> { return [ { @@ -71,13 +73,18 @@ export function getColumns({ name: getLatencyColumnLabel(latencyAggregationType), width: px(unit * 10), render: (_, { latency, name }) => { - const timeseries = - transactionGroupComparisonStatistics?.[name]?.latency; + const currentTimeseries = + transactionGroupComparisonStatistics?.currentPeriod?.[name]?.latency; + const previousTimeseries = + transactionGroupComparisonStatistics?.previousPeriod?.[name]?.latency; return ( ); @@ -92,13 +99,20 @@ export function getColumns({ ), width: px(unit * 10), render: (_, { throughput, name }) => { - const timeseries = - transactionGroupComparisonStatistics?.[name]?.throughput; + const currentTimeseries = + transactionGroupComparisonStatistics?.currentPeriod?.[name] + ?.throughput; + const previousTimeseries = + transactionGroupComparisonStatistics?.previousPeriod?.[name] + ?.throughput; return ( ); @@ -113,13 +127,20 @@ export function getColumns({ ), width: px(unit * 8), render: (_, { errorRate, name }) => { - const timeseries = - transactionGroupComparisonStatistics?.[name]?.errorRate; + const currentTimeseries = + transactionGroupComparisonStatistics?.currentPeriod?.[name] + ?.errorRate; + const previousTimeseries = + transactionGroupComparisonStatistics?.previousPeriod?.[name] + ?.errorRate; return ( ); @@ -134,9 +155,24 @@ export function getColumns({ ), width: px(unit * 5), render: (_, { name }) => { - const impact = - transactionGroupComparisonStatistics?.[name]?.impact ?? 0; - return ; + const currentImpact = + transactionGroupComparisonStatistics?.currentPeriod?.[name]?.impact ?? + 0; + const previousImpact = + transactionGroupComparisonStatistics?.previousPeriod?.[name] + ?.impact ?? 0; + return ( + + + + + {comparisonEnabled && ( + + + + )} + + ); }, }, ]; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx index e4c42b4de90fe..02f60eab2cb88 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx @@ -20,6 +20,7 @@ import { useUrlParams } from '../../../../context/url_params_context/use_url_par import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { TransactionOverviewLink } from '../../../shared/Links/apm/transaction_overview_link'; 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_columns'; @@ -31,6 +32,7 @@ const INITIAL_STATE = { transactionGroups: [], isAggregationAccurate: true, requestId: '', + transactionGroupsTotalItems: 0, }; type SortField = 'name' | 'latency' | 'throughput' | 'errorRate' | 'impact'; @@ -54,12 +56,27 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) { }); const { pageIndex, sort } = tableOptions; + const { direction, field } = sort; const { transactionType } = useApmServiceContext(); const { - urlParams: { environment, kuery, start, end, latencyAggregationType }, + urlParams: { + start, + end, + latencyAggregationType, + comparisonType, + comparisonEnabled, + environment, + kuery, + }, } = useUrlParams(); + const { comparisonStart, comparisonEnd } = getTimeRangeComparison({ + start, + end, + comparisonType, + }); + const { data = INITIAL_STATE, status } = useFetcher( (callApmApi) => { if (!start || !end || !latencyAggregationType || !transactionType) { @@ -80,12 +97,23 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) { }, }, }).then((response) => { + const currentPageTransactionGroups = orderBy( + response.transactionGroups, + field, + direction + ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); + return { - requestId: uuid(), ...response, + // Everytime the primary statistics is refetched, updates the requestId making the comparison API to be refetched. + requestId: uuid(), + transactionGroupsTotalItems: response.transactionGroups.length, + transactionGroups: currentPageTransactionGroups, }; }); }, + // 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, @@ -94,19 +122,14 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) { end, transactionType, latencyAggregationType, + pageIndex, + direction, + field, + comparisonType, ] ); - const { transactionGroups, requestId } = data; - const currentPageTransactionGroups = orderBy( - transactionGroups, - sort.field, - sort.direction - ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); - - const transactionNames = JSON.stringify( - currentPageTransactionGroups.map(({ name }) => name).sort() - ); + const { transactionGroups, requestId, transactionGroupsTotalItems } = data; const { data: transactionGroupComparisonStatistics, @@ -114,7 +137,7 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) { } = useFetcher( (callApmApi) => { if ( - currentPageTransactionGroups.length && + transactionGroupsTotalItems && start && end && transactionType && @@ -133,15 +156,19 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) { numBuckets: 20, transactionType, latencyAggregationType, - transactionNames, + transactionNames: JSON.stringify( + transactionGroups.map(({ name }) => name).sort() + ), + comparisonStart, + comparisonEnd, }, }, }); } }, - // only fetches statistics when requestId changes or transaction names changes + // only fetches comparison statistics when requestId is invalidated by primary statistics api call // eslint-disable-next-line react-hooks/exhaustive-deps - [requestId, transactionNames], + [requestId], { preservePreviousData: false } ); @@ -149,6 +176,7 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) { serviceName, latencyAggregationType, transactionGroupComparisonStatistics, + comparisonEnabled, }); const isLoading = @@ -158,17 +186,10 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) { const pagination = { pageIndex, pageSize: PAGE_SIZE, - totalItemCount: transactionGroups.length, + totalItemCount: transactionGroupsTotalItems, hidePerPageOptions: true, }; - const sorting = { - sort: { - field: sort.field, - direction: sort.direction, - }, - }; - return ( @@ -204,14 +225,14 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) { { value: number; - size?: 'l' | 'm'; + size?: 's' | 'l' | 'm'; max?: number; + color?: string; } export function ImpactBar({ value, size = 'l', max = 100, + color = 'primary', ...rest }: ImpactBarProps) { return ( - + ); } diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index a89d36f708990..59205ef498534 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -12,6 +12,7 @@ import { AreaSeries, Chart, CurveType, + LineSeries, ScaleType, Settings, } from '@elastic/charts'; @@ -20,8 +21,9 @@ import { Coordinate } from '../../../../../typings/timeseries'; import { useChartTheme } from '../../../../../../observability/public'; import { px, unit } from '../../../../style/variables'; import { useTheme } from '../../../../hooks/use_theme'; +import { getComparisonChartTheme } from '../../time_comparison/get_time_range_comparison'; -type Color = +export type Color = | 'euiColorVis0' | 'euiColorVis1' | 'euiColorVis2' @@ -33,9 +35,16 @@ type Color = | 'euiColorVis8' | 'euiColorVis9'; +function hasValidTimeseries( + series?: Coordinate[] | null +): series is Coordinate[] { + return !!series?.some((point) => point.y !== null); +} + export function SparkPlot({ color, series, + comparisonSeries = [], valueLabel, compact, }: { @@ -43,9 +52,12 @@ export function SparkPlot({ series?: Coordinate[] | null; valueLabel: React.ReactNode; compact?: boolean; + comparisonSeries?: Coordinate[]; }) { const theme = useTheme(); const defaultChartTheme = useChartTheme(); + const comparisonChartTheme = getComparisonChartTheme(theme); + const hasComparisonSeries = !!comparisonSeries?.length; const sparkplotChartTheme = merge({}, defaultChartTheme, { chartMargins: { left: 0, right: 0, top: 0, bottom: 0 }, @@ -55,6 +67,7 @@ export function SparkPlot({ areaSeriesStyle: { point: { opacity: 0 }, }, + ...(hasComparisonSeries ? comparisonChartTheme : {}), }); const colorValue = theme.eui[color]; @@ -64,29 +77,20 @@ export function SparkPlot({ width: compact ? px(unit * 3) : px(unit * 4), }; + const Sparkline = hasComparisonSeries ? LineSeries : AreaSeries; + return ( - {!series || series.every((point) => point.y === null) ? ( -
- -
- ) : ( + {hasValidTimeseries(series) ? ( - + {hasComparisonSeries && ( + + )} + ) : ( +
+ +
)}
diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.ts b/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.ts index 5dd014441a9e4..e436f65e85ad9 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.ts +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/get_time_range_comparison.ts @@ -43,11 +43,11 @@ export function getTimeRangeComparison({ start, end, }: { - comparisonType: TimeRangeComparisonType; + comparisonType?: TimeRangeComparisonType; start?: string; end?: string; }) { - if (!start || !end) { + if (!comparisonType || !start || !end) { return {}; } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_comparison_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_comparison_statistics.ts index 6875a41ad7d9f..ebdb5fb5bbdc2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_comparison_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_comparison_statistics.ts @@ -20,6 +20,7 @@ import { kqlQuery, } from '../../../server/utils/queries'; import { Coordinate } from '../../../typings/timeseries'; +import { offsetPreviousPeriodCoordinates } from '../../utils/offset_previous_period_coordinate'; import { withApmSpan } from '../../utils/with_apm_span'; import { getDocumentTypeFilterForAggregatedTransactions, @@ -44,31 +45,35 @@ export async function getServiceTransactionGroupComparisonStatistics({ searchAggregatedTransactions, transactionType, latencyAggregationType, + start, + end, + getOffsetXCoordinate, }: { environment?: string; kuery?: string; serviceName: string; transactionNames: string[]; - setup: Setup & SetupTimeRange; + setup: Setup; numBuckets: number; searchAggregatedTransactions: boolean; transactionType: string; latencyAggregationType: LatencyAggregationType; + start: number; + end: number; + getOffsetXCoordinate?: (timeseries: Coordinate[]) => Coordinate[]; }): Promise< - Record< - string, - { - latency: Coordinate[]; - throughput: Coordinate[]; - errorRate: Coordinate[]; - impact: number; - } - > + Array<{ + transactionName: string; + latency: Coordinate[]; + throughput: Coordinate[]; + errorRate: Coordinate[]; + impact: number; + }> > { return withApmSpan( 'get_service_transaction_group_comparison_statistics', async () => { - const { apmEventClient, start, end } = setup; + const { apmEventClient } = setup; const { intervalString } = getBucketSize({ start, end, numBuckets }); const field = getTransactionDurationFieldForAggregatedTransactions( @@ -145,44 +150,116 @@ export async function getServiceTransactionGroupComparisonStatistics({ const buckets = response.aggregations?.transaction_groups.buckets ?? []; const totalDuration = response.aggregations?.total_duration.value; - return keyBy( - buckets.map((bucket) => { - const transactionName = bucket.key; - const latency = bucket.timeseries.buckets.map((timeseriesBucket) => ({ + return buckets.map((bucket) => { + const transactionName = bucket.key as string; + const latency = bucket.timeseries.buckets.map((timeseriesBucket) => ({ + x: timeseriesBucket.key, + y: getLatencyValue({ + latencyAggregationType, + aggregation: timeseriesBucket.latency, + }), + })); + const throughput = bucket.timeseries.buckets.map( + (timeseriesBucket) => ({ x: timeseriesBucket.key, - y: getLatencyValue({ - latencyAggregationType, - aggregation: timeseriesBucket.latency, - }), - })); - const throughput = bucket.timeseries.buckets.map( - (timeseriesBucket) => ({ - x: timeseriesBucket.key, - y: timeseriesBucket.throughput_rate.value, - }) - ); - const errorRate = bucket.timeseries.buckets.map( - (timeseriesBucket) => ({ - x: timeseriesBucket.key, - y: calculateTransactionErrorPercentage( - timeseriesBucket[EVENT_OUTCOME] - ), - }) - ); - const transactionGroupTotalDuration = - bucket.transaction_group_total_duration.value || 0; - return { - transactionName, - latency, - throughput, - errorRate, - impact: totalDuration - ? (transactionGroupTotalDuration * 100) / totalDuration - : 0, - }; - }), - 'transactionName' - ); + y: timeseriesBucket.throughput_rate.value, + }) + ); + const errorRate = bucket.timeseries.buckets.map((timeseriesBucket) => ({ + x: timeseriesBucket.key, + y: calculateTransactionErrorPercentage( + timeseriesBucket[EVENT_OUTCOME] + ), + })); + const transactionGroupTotalDuration = + bucket.transaction_group_total_duration.value || 0; + return { + transactionName, + latency: getOffsetXCoordinate + ? getOffsetXCoordinate(latency) + : latency, + throughput: getOffsetXCoordinate + ? getOffsetXCoordinate(throughput) + : throughput, + errorRate: getOffsetXCoordinate + ? getOffsetXCoordinate(errorRate) + : errorRate, + impact: totalDuration + ? (transactionGroupTotalDuration * 100) / totalDuration + : 0, + }; + }); } ); } + +export async function getServiceTransactionGroupComparisonStatisticsPeriods({ + serviceName, + transactionNames, + setup, + numBuckets, + searchAggregatedTransactions, + transactionType, + latencyAggregationType, + comparisonStart, + comparisonEnd, + environment, + kuery, +}: { + serviceName: string; + transactionNames: string[]; + setup: Setup & SetupTimeRange; + numBuckets: number; + searchAggregatedTransactions: boolean; + transactionType: string; + latencyAggregationType: LatencyAggregationType; + comparisonStart?: number; + comparisonEnd?: number; + environment?: string; + kuery?: string; +}) { + const { start, end } = setup; + + const commonProps = { + setup, + serviceName, + transactionNames, + searchAggregatedTransactions, + transactionType, + numBuckets, + latencyAggregationType: latencyAggregationType as LatencyAggregationType, + environment, + kuery, + }; + + const currentPeriodPromise = getServiceTransactionGroupComparisonStatistics({ + ...commonProps, + start, + end, + }); + + const previousPeriodPromise = + comparisonStart && comparisonEnd + ? getServiceTransactionGroupComparisonStatistics({ + ...commonProps, + start: comparisonStart, + end: comparisonEnd, + getOffsetXCoordinate: (timeseries: Coordinate[]) => + offsetPreviousPeriodCoordinates({ + currentPeriodStart: start, + previousPeriodStart: comparisonStart, + previousPeriodTimeseries: timeseries, + }), + }) + : []; + + const [currentPeriod, previousPeriod] = await Promise.all([ + currentPeriodPromise, + previousPeriodPromise, + ]); + + return { + currentPeriod: keyBy(currentPeriod, 'transactionName'), + previousPeriod: keyBy(previousPeriod, 'transactionName'), + }; +} diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index 330b4b4bdd12a..f7ff93d104647 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -15,7 +15,7 @@ import { toNumberRt } from '../../common/runtime_types/to_number_rt'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceTransactionGroups } from '../lib/services/get_service_transaction_groups'; -import { getServiceTransactionGroupComparisonStatistics } from '../lib/services/get_service_transaction_group_comparison_statistics'; +import { getServiceTransactionGroupComparisonStatisticsPeriods } from '../lib/services/get_service_transaction_group_comparison_statistics'; import { getTransactionBreakdown } from '../lib/transactions/breakdown'; import { getTransactionDistribution } from '../lib/transactions/distribution'; import { getAnomalySeries } from '../lib/transactions/get_anomaly_data'; @@ -24,7 +24,12 @@ import { getThroughputCharts } from '../lib/transactions/get_throughput_charts'; import { getTransactionGroupList } from '../lib/transaction_groups'; import { getErrorRate } from '../lib/transaction_groups/get_error_rate'; import { createRoute } from './create_route'; -import { environmentRt, kueryRt, rangeRt } from './default_api_types'; +import { + environmentRt, + comparisonRangeRt, + rangeRt, + kueryRt, +} from './default_api_types'; /** * Returns a list of transactions grouped by name @@ -118,6 +123,7 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ environmentRt, kueryRt, rangeRt, + comparisonRangeRt, t.type({ transactionNames: jsonRt.pipe(t.array(t.string)), numBuckets: toNumberRt, @@ -145,10 +151,12 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ latencyAggregationType, numBuckets, transactionType, + comparisonStart, + comparisonEnd, }, } = context.params; - return getServiceTransactionGroupComparisonStatistics({ + return await getServiceTransactionGroupComparisonStatisticsPeriods({ environment, kuery, setup, @@ -158,6 +166,8 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ transactionType, numBuckets, latencyAggregationType: latencyAggregationType as LatencyAggregationType, + comparisonStart, + comparisonEnd, }); }, }); diff --git a/x-pack/plugins/apm/server/utils/offset_previous_period_coordinate.ts b/x-pack/plugins/apm/server/utils/offset_previous_period_coordinate.ts index 837e3d02056f0..7ca153ec6181f 100644 --- a/x-pack/plugins/apm/server/utils/offset_previous_period_coordinate.ts +++ b/x-pack/plugins/apm/server/utils/offset_previous_period_coordinate.ts @@ -21,12 +21,10 @@ export function offsetPreviousPeriodCoordinates({ return []; } - const dateOffset = moment(currentPeriodStart).diff( - moment(previousPeriodStart) - ); + const dateDiff = currentPeriodStart - previousPeriodStart; return previousPeriodTimeseries.map(({ x, y }) => { - const offsetX = moment(x).add(dateOffset).valueOf(); + const offsetX = moment(x).add(dateDiff).valueOf(); return { x: offsetX, y, diff --git a/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/transactions_groups_comparison_statistics.snap b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/transactions_groups_comparison_statistics.snap index 739ff5a080d76..bc641ad1a9890 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/transactions_groups_comparison_statistics.snap +++ b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/transactions_groups_comparison_statistics.snap @@ -1,5 +1,419 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns data with previous period returns correct error rate data 1`] = ` +Array [ + Object { + "x": 1607436720000, + "y": null, + }, + Object { + "x": 1607436780000, + "y": null, + }, + Object { + "x": 1607436840000, + "y": null, + }, + Object { + "x": 1607436900000, + "y": null, + }, + Object { + "x": 1607436960000, + "y": 0, + }, + Object { + "x": 1607437020000, + "y": null, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": null, + }, + Object { + "x": 1607437200000, + "y": 0.5, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": null, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": null, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": null, + }, +] +`; + +exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns data with previous period returns correct error rate data 2`] = ` +Array [ + Object { + "x": 1607436720000, + "y": null, + }, + Object { + "x": 1607436780000, + "y": 0, + }, + Object { + "x": 1607436840000, + "y": 0, + }, + Object { + "x": 1607436900000, + "y": 0, + }, + Object { + "x": 1607436960000, + "y": null, + }, + Object { + "x": 1607437020000, + "y": 0, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": 0, + }, + Object { + "x": 1607437200000, + "y": null, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": 0, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": 0, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": null, + }, +] +`; + +exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns data with previous period returns correct latency data 1`] = ` +Array [ + Object { + "x": 1607436720000, + "y": null, + }, + Object { + "x": 1607436780000, + "y": null, + }, + Object { + "x": 1607436840000, + "y": null, + }, + Object { + "x": 1607436900000, + "y": null, + }, + Object { + "x": 1607436960000, + "y": 30501, + }, + Object { + "x": 1607437020000, + "y": null, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": null, + }, + Object { + "x": 1607437200000, + "y": 46937.5, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": null, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": null, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": null, + }, +] +`; + +exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns data with previous period returns correct latency data 2`] = ` +Array [ + Object { + "x": 1607436720000, + "y": null, + }, + Object { + "x": 1607436780000, + "y": 69429, + }, + Object { + "x": 1607436840000, + "y": 8071285, + }, + Object { + "x": 1607436900000, + "y": 31949, + }, + Object { + "x": 1607436960000, + "y": null, + }, + Object { + "x": 1607437020000, + "y": 47755, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": 35403, + }, + Object { + "x": 1607437200000, + "y": null, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": 48137, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": 35457, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": null, + }, +] +`; + +exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns data with previous period returns correct throughput data 1`] = ` +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": 2, + }, + 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": 0, + }, + 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, + }, +] +`; + +exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns data with previous period returns correct throughput data 2`] = ` +Array [ + Object { + "x": 1607436720000, + "y": 0, + }, + Object { + "x": 1607436780000, + "y": 1, + }, + Object { + "x": 1607436840000, + "y": 2, + }, + Object { + "x": 1607436900000, + "y": 1, + }, + Object { + "x": 1607436960000, + "y": 0, + }, + Object { + "x": 1607437020000, + "y": 1, + }, + Object { + "x": 1607437080000, + "y": 0, + }, + Object { + "x": 1607437140000, + "y": 4, + }, + Object { + "x": 1607437200000, + "y": 0, + }, + Object { + "x": 1607437260000, + "y": 0, + }, + Object { + "x": 1607437320000, + "y": 0, + }, + Object { + "x": 1607437380000, + "y": 2, + }, + Object { + "x": 1607437440000, + "y": 0, + }, + Object { + "x": 1607437500000, + "y": 1, + }, + Object { + "x": 1607437560000, + "y": 0, + }, + Object { + "x": 1607437620000, + "y": 0, + }, +] +`; + exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns the correct data 1`] = ` Array [ Object { @@ -387,7 +801,7 @@ Array [ ] `; -exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns the correct for latency aggregation 99th percentile 1`] = ` +exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns the correct data for latency aggregation 99th percentile 1`] = ` Array [ Object { "x": 1607435820000, diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_comparison_statistics.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_comparison_statistics.ts index fdc499594aad0..1065bc9405838 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_comparison_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_comparison_statistics.ts @@ -7,6 +7,8 @@ import expect from '@kbn/expect'; import url from 'url'; +import moment from 'moment'; +import { pick } from 'lodash'; import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -42,7 +44,7 @@ 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: {} }); }); } ); @@ -68,19 +70,30 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); - const transactionsGroupsComparisonStatistics = response.body as TransactionsGroupsComparisonStatistics; + const { + currentPeriod, + previousPeriod, + } = response.body as TransactionsGroupsComparisonStatistics; - expect(Object.keys(transactionsGroupsComparisonStatistics).length).to.be.eql( - transactionNames.length - ); + expect(Object.keys(currentPeriod).sort()).to.be.eql(transactionNames.sort()); + + const currentPeriodItems = Object.values(currentPeriod).map((data) => data); + const previousPeriodItems = Object.values(previousPeriod).map((data) => data); - transactionNames.map((transactionName) => { - expect(transactionsGroupsComparisonStatistics[transactionName]).not.to.be.empty(); + expect(previousPeriodItems.length).to.be.eql(0); + + transactionNames.forEach((transactionName) => { + expect(currentPeriod[transactionName]).not.to.be.empty(); }); - const { latency, throughput, errorRate, impact } = transactionsGroupsComparisonStatistics[ - transactionNames[0] - ]; + const firstItem = currentPeriodItems[0]; + const { latency, throughput, errorRate, impact } = pick( + firstItem, + 'latency', + 'throughput', + 'errorRate', + 'impact' + ); expect(removeEmptyCoordinates(latency).length).to.be.greaterThan(0); expectSnapshot(latency).toMatch(); @@ -94,7 +107,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(roundNumber(impact)).toMatchInline(`"93.93"`); }); - it('returns the correct for latency aggregation 99th percentile', async () => { + it('returns the correct data for latency aggregation 99th percentile', async () => { const response = await supertest.get( url.format({ pathname: `/api/apm/services/opbeans-java/transactions/groups/comparison_statistics`, @@ -111,19 +124,30 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); - const transactionsGroupsComparisonStatistics = response.body as TransactionsGroupsComparisonStatistics; + const { + currentPeriod, + previousPeriod, + } = response.body as TransactionsGroupsComparisonStatistics; - expect(Object.keys(transactionsGroupsComparisonStatistics).length).to.be.eql( - transactionNames.length - ); + expect(Object.keys(currentPeriod).sort()).to.be.eql(transactionNames.sort()); + + const currentPeriodItems = Object.values(currentPeriod); + const previousPeriodItems = Object.values(previousPeriod); - transactionNames.map((transactionName) => { - expect(transactionsGroupsComparisonStatistics[transactionName]).not.to.be.empty(); + expect(previousPeriodItems).to.be.empty(); + + transactionNames.forEach((transactionName) => { + expect(currentPeriod[transactionName]).not.to.be.empty(); }); - const { latency, throughput, errorRate } = transactionsGroupsComparisonStatistics[ - transactionNames[0] - ]; + const firstItem = currentPeriodItems[0]; + const { latency, throughput, errorRate } = pick( + firstItem, + 'latency', + 'throughput', + 'errorRate' + ); + expect(removeEmptyCoordinates(latency).length).to.be.greaterThan(0); expectSnapshot(latency).toMatch(); @@ -147,7 +171,105 @@ 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: {} }); + }); + + describe('returns data with previous period', async () => { + let currentPeriod: TransactionsGroupsComparisonStatistics['currentPeriod']; + let previousPeriod: TransactionsGroupsComparisonStatistics['previousPeriod']; + before(async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/transactions/groups/comparison_statistics`, + query: { + numBuckets: 20, + transactionType: 'request', + latencyAggregationType: 'avg', + transactionNames: JSON.stringify(transactionNames), + start: moment(end).subtract(15, 'minutes').toISOString(), + end, + comparisonStart: start, + comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + }, + }) + ); + + expect(response.status).to.be(200); + currentPeriod = response.body.currentPeriod; + previousPeriod = response.body.previousPeriod; + }); + + it('returns correct number of items', () => { + expect(Object.keys(currentPeriod).sort()).to.be.eql(transactionNames.sort()); + expect(Object.keys(previousPeriod).sort()).to.be.eql(transactionNames.sort()); + + transactionNames.forEach((transactionName) => { + expect(currentPeriod[transactionName]).not.to.be.empty(); + expect(previousPeriod[transactionName]).not.to.be.empty(); + }); + }); + + it('returns correct latency data', () => { + const currentPeriodItems = Object.values(currentPeriod).map((data) => data); + const previousPeriodItems = Object.values(previousPeriod).map((data) => data); + + const currentPeriodFirstItem = currentPeriodItems[0]; + const previousPeriodFirstItem = previousPeriodItems[0]; + + expect(removeEmptyCoordinates(currentPeriodFirstItem.latency).length).to.be.greaterThan( + 0 + ); + expect(removeEmptyCoordinates(previousPeriodFirstItem.latency).length).to.be.greaterThan( + 0 + ); + expectSnapshot(currentPeriodFirstItem.latency).toMatch(); + expectSnapshot(previousPeriodFirstItem.latency).toMatch(); + }); + + it('returns correct throughput data', () => { + const currentPeriodItems = Object.values(currentPeriod).map((data) => data); + const previousPeriodItems = Object.values(previousPeriod).map((data) => data); + + const currentPeriodFirstItem = currentPeriodItems[0]; + const previousPeriodFirstItem = previousPeriodItems[0]; + + expect( + removeEmptyCoordinates(currentPeriodFirstItem.throughput).length + ).to.be.greaterThan(0); + expect( + removeEmptyCoordinates(previousPeriodFirstItem.throughput).length + ).to.be.greaterThan(0); + expectSnapshot(currentPeriodFirstItem.throughput).toMatch(); + expectSnapshot(previousPeriodFirstItem.throughput).toMatch(); + }); + + it('returns correct error rate data', () => { + const currentPeriodItems = Object.values(currentPeriod).map((data) => data); + const previousPeriodItems = Object.values(previousPeriod).map((data) => data); + + const currentPeriodFirstItem = currentPeriodItems[0]; + const previousPeriodFirstItem = previousPeriodItems[0]; + + expect(removeEmptyCoordinates(currentPeriodFirstItem.errorRate).length).to.be.greaterThan( + 0 + ); + expect( + removeEmptyCoordinates(previousPeriodFirstItem.errorRate).length + ).to.be.greaterThan(0); + expectSnapshot(currentPeriodFirstItem.errorRate).toMatch(); + expectSnapshot(previousPeriodFirstItem.errorRate).toMatch(); + }); + + it('returns correct impact data', () => { + const currentPeriodItems = Object.values(currentPeriod).map((data) => data); + const previousPeriodItems = Object.values(previousPeriod).map((data) => data); + + const currentPeriodFirstItem = currentPeriodItems[0]; + const previousPeriodFirstItem = previousPeriodItems[0]; + + expectSnapshot(roundNumber(currentPeriodFirstItem.impact)).toMatchInline(`"21.75"`); + expectSnapshot(roundNumber(previousPeriodFirstItem.impact)).toMatchInline(`"96.94"`); + }); }); } );