[APM] Api tests for Error distribution api (#116674)

* adding tests for error distribution

* addressing pr changes

* addressing pr comments

* fixing ts issues
This commit is contained in:
Cauê Marcondes 2021-10-29 16:57:08 -04:00 committed by GitHub
parent f32c334ad2
commit 7130d6eb45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 249 additions and 56 deletions

View file

@ -45,40 +45,40 @@ export function Example() {
const distribution = {
bucketSize: 62350,
currentPeriod: [
{ key: 1624279912350, count: 6 },
{ key: 1624279974700, count: 1 },
{ key: 1624280037050, count: 2 },
{ key: 1624280099400, count: 3 },
{ key: 1624280161750, count: 13 },
{ key: 1624280224100, count: 1 },
{ key: 1624280286450, count: 2 },
{ key: 1624280348800, count: 0 },
{ key: 1624280411150, count: 4 },
{ key: 1624280473500, count: 4 },
{ key: 1624280535850, count: 1 },
{ key: 1624280598200, count: 4 },
{ key: 1624280660550, count: 0 },
{ key: 1624280722900, count: 2 },
{ key: 1624280785250, count: 3 },
{ key: 1624280847600, count: 0 },
{ x: 1624279912350, y: 6 },
{ x: 1624279974700, y: 1 },
{ x: 1624280037050, y: 2 },
{ x: 1624280099400, y: 3 },
{ x: 1624280161750, y: 13 },
{ x: 1624280224100, y: 1 },
{ x: 1624280286450, y: 2 },
{ x: 1624280348800, y: 0 },
{ x: 1624280411150, y: 4 },
{ x: 1624280473500, y: 4 },
{ x: 1624280535850, y: 1 },
{ x: 1624280598200, y: 4 },
{ x: 1624280660550, y: 0 },
{ x: 1624280722900, y: 2 },
{ x: 1624280785250, y: 3 },
{ x: 1624280847600, y: 0 },
],
previousPeriod: [
{ key: 1624279912350, count: 6 },
{ key: 1624279974700, count: 1 },
{ key: 1624280037050, count: 2 },
{ key: 1624280099400, count: 3 },
{ key: 1624280161750, count: 13 },
{ key: 1624280224100, count: 1 },
{ key: 1624280286450, count: 2 },
{ key: 1624280348800, count: 0 },
{ key: 1624280411150, count: 4 },
{ key: 1624280473500, count: 4 },
{ key: 1624280535850, count: 1 },
{ key: 1624280598200, count: 4 },
{ key: 1624280660550, count: 0 },
{ key: 1624280722900, count: 2 },
{ key: 1624280785250, count: 3 },
{ key: 1624280847600, count: 0 },
{ x: 1624279912350, y: 6 },
{ x: 1624279974700, y: 1 },
{ x: 1624280037050, y: 2 },
{ x: 1624280099400, y: 3 },
{ x: 1624280161750, y: 13 },
{ x: 1624280224100, y: 1 },
{ x: 1624280286450, y: 2 },
{ x: 1624280348800, y: 0 },
{ x: 1624280411150, y: 4 },
{ x: 1624280473500, y: 4 },
{ x: 1624280535850, y: 1 },
{ x: 1624280598200, y: 4 },
{ x: 1624280660550, y: 0 },
{ x: 1624280722900, y: 2 },
{ x: 1624280785250, y: 3 },
{ x: 1624280847600, y: 0 },
],
};

View file

@ -22,7 +22,6 @@ import { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED } from '@kbn/rule-da
import { i18n } from '@kbn/i18n';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { offsetPreviousPeriodCoordinates } from '../../../../../common/utils/offset_previous_period_coordinate';
import { useTheme } from '../../../../hooks/use_theme';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { AlertType } from '../../../../../common/alert_types';
@ -31,7 +30,6 @@ import { ChartContainer } from '../../../shared/charts/chart_container';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { LazyAlertsFlyout } from '../../../../../../observability/public';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { Coordinate } from '../../../../../typings/timeseries';
import { getTimeZone } from '../../../shared/charts/helper/timezone';
const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED =
@ -40,18 +38,6 @@ const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED =
type ErrorDistributionAPIResponse =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/distribution'>;
export function getCoordinatedBuckets(
buckets:
| ErrorDistributionAPIResponse['currentPeriod']
| ErrorDistributionAPIResponse['previousPeriod']
): Coordinate[] {
return buckets.map(({ count, key }) => {
return {
x: key,
y: count,
};
});
}
interface Props {
fetchStatus: FETCH_STATUS;
distribution: ErrorDistributionAPIResponse;
@ -61,15 +47,13 @@ interface Props {
export function ErrorDistribution({ distribution, title, fetchStatus }: Props) {
const { core } = useApmPluginContext();
const theme = useTheme();
const currentPeriod = getCoordinatedBuckets(distribution.currentPeriod);
const previousPeriod = getCoordinatedBuckets(distribution.previousPeriod);
const { urlParams } = useUrlParams();
const { comparisonEnabled } = urlParams;
const timeseries = [
{
data: currentPeriod,
data: distribution.currentPeriod,
color: theme.eui.euiColorVis1,
title: i18n.translate('xpack.apm.errorGroup.chart.ocurrences', {
defaultMessage: 'Occurences',
@ -78,10 +62,7 @@ export function ErrorDistribution({ distribution, title, fetchStatus }: Props) {
...(comparisonEnabled
? [
{
data: offsetPreviousPeriodCoordinates({
currentPeriodTimeseries: currentPeriod,
previousPeriodTimeseries: previousPeriod,
}),
data: distribution.previousPeriod,
color: theme.eui.euiColorMediumShade,
title: i18n.translate(
'xpack.apm.errorGroup.chart.ocurrences.previousPeriodLabel',

View file

@ -80,8 +80,8 @@ export async function getBuckets({
const buckets = (resp.aggregations?.distribution.buckets || []).map(
(bucket) => ({
key: bucket.key,
count: bucket.doc_count,
x: bucket.key,
y: bucket.doc_count,
})
);

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { offsetPreviousPeriodCoordinates } from '../../../../common/utils/offset_previous_period_coordinate';
import { Setup } from '../../helpers/setup_request';
import { BUCKET_TARGET_COUNT } from '../../transactions/constants';
import { getBuckets } from './get_buckets';
@ -64,7 +65,10 @@ export async function getErrorDistribution({
return {
currentPeriod: currentPeriod.buckets,
previousPeriod: previousPeriod.buckets,
previousPeriod: offsetPreviousPeriodCoordinates({
currentPeriodTimeseries: currentPeriod.buckets,
previousPeriodTimeseries: previousPeriod.buckets,
}),
bucketSize,
};
}

View file

@ -0,0 +1,204 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { service, timerange } from '@elastic/apm-synthtrace';
import expect from '@kbn/expect';
import { first, last, sumBy } from 'lodash';
import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_number';
import {
APIClientRequestParamsOf,
APIReturnType,
} from '../../../../plugins/apm/public/services/rest/createCallApmApi';
import { RecursivePartial } from '../../../../plugins/apm/typings/common';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { registry } from '../../common/registry';
type ErrorsDistribution =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/distribution'>;
export default function ApiTest({ getService }: FtrProviderContext) {
const apmApiClient = getService('apmApiClient');
const synthtraceEsClient = getService('synthtraceEsClient');
const serviceName = 'synth-go';
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
async function callApi(
overrides?: RecursivePartial<
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/distribution'>['params']
>
) {
const response = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/services/{serviceName}/errors/distribution',
params: {
path: {
serviceName,
...overrides?.path,
},
query: {
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
environment: 'ENVIRONMENT_ALL',
kuery: '',
...overrides?.query,
},
},
});
return response;
}
registry.when('when data is not loaded', { config: 'basic', archives: [] }, () => {
it('handles the empty state', async () => {
const response = await callApi();
expect(response.status).to.be(200);
expect(response.body.currentPeriod.length).to.be(0);
expect(response.body.previousPeriod.length).to.be(0);
});
});
registry.when(
'when data is loaded',
{ config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
() => {
describe('errors distribution', () => {
const appleTransaction = {
name: 'GET /apple 🍎 ',
successRate: 75,
failureRate: 25,
};
const bananaTransaction = {
name: 'GET /banana 🍌',
successRate: 50,
failureRate: 50,
};
before(async () => {
const serviceGoProdInstance = service(serviceName, 'production', 'go').instance(
'instance-a'
);
const interval = '1m';
const indices = [appleTransaction, bananaTransaction]
.map((transaction, index) => {
return [
...timerange(start, end)
.interval(interval)
.rate(transaction.successRate)
.flatMap((timestamp) =>
serviceGoProdInstance
.transaction(transaction.name)
.timestamp(timestamp)
.duration(1000)
.success()
.serialize()
),
...timerange(start, end)
.interval(interval)
.rate(transaction.failureRate)
.flatMap((timestamp) =>
serviceGoProdInstance
.transaction(transaction.name)
.errors(
serviceGoProdInstance
.error(`Error ${index}`, transaction.name)
.timestamp(timestamp)
)
.duration(1000)
.timestamp(timestamp)
.failure()
.serialize()
),
];
})
.flatMap((_) => _);
await synthtraceEsClient.index(indices);
});
after(() => synthtraceEsClient.clean());
describe('without comparison', () => {
let errorsDistribution: ErrorsDistribution;
before(async () => {
const response = await callApi();
errorsDistribution = response.body;
});
it('displays combined number of occurrences', () => {
const countSum = sumBy(errorsDistribution.currentPeriod, 'y');
const numberOfBuckets = 15;
expect(countSum).to.equal(
(appleTransaction.failureRate + bananaTransaction.failureRate) * numberOfBuckets
);
});
});
describe('displays occurrences for type "apple transaction" only', () => {
let errorsDistribution: ErrorsDistribution;
before(async () => {
const response = await callApi({
query: { kuery: `error.exception.type:"${appleTransaction.name}"` },
});
errorsDistribution = response.body;
});
it('displays combined number of occurrences', () => {
const countSum = sumBy(errorsDistribution.currentPeriod, 'y');
const numberOfBuckets = 15;
expect(countSum).to.equal(appleTransaction.failureRate * numberOfBuckets);
});
});
describe('with comparison', () => {
let errorsDistribution: ErrorsDistribution;
before(async () => {
const fiveMinutes = 5 * 60 * 1000;
const response = await callApi({
query: {
start: new Date(end - fiveMinutes).toISOString(),
end: new Date(end).toISOString(),
comparisonStart: new Date(start).toISOString(),
comparisonEnd: new Date(start + fiveMinutes).toISOString(),
},
});
errorsDistribution = response.body;
});
it('returns some data', () => {
const hasCurrentPeriodData = errorsDistribution.currentPeriod.some(({ y }) =>
isFiniteNumber(y)
);
const hasPreviousPeriodData = errorsDistribution.previousPeriod.some(({ y }) =>
isFiniteNumber(y)
);
expect(hasCurrentPeriodData).to.equal(true);
expect(hasPreviousPeriodData).to.equal(true);
});
it('has same start time for both periods', () => {
expect(first(errorsDistribution.currentPeriod)?.x).to.equal(
first(errorsDistribution.previousPeriod)?.x
);
});
it('has same end time for both periods', () => {
expect(last(errorsDistribution.currentPeriod)?.x).to.equal(
last(errorsDistribution.previousPeriod)?.x
);
});
it('returns same number of buckets for both periods', () => {
expect(errorsDistribution.currentPeriod.length).to.equal(
errorsDistribution.previousPeriod.length
);
});
});
});
}
);
}

View file

@ -241,6 +241,10 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte
loadTestFile(require.resolve('./latency/service_apis'));
});
describe('errors/distribution', function () {
loadTestFile(require.resolve('./errors/distribution'));
});
registry.run(providerContext);
});
}