diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/distribution.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/distribution.spec.ts new file mode 100644 index 0000000000000..3c80c8df83018 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/distribution.spec.ts @@ -0,0 +1,207 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { first, last, sumBy } from 'lodash'; +import { + APIClientRequestParamsOf, + APIReturnType, +} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client'; +import { config, generateData } from './generate_data'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { isFiniteNumber } from '../utils/common'; + +type ErrorsDistribution = + APIReturnType<'GET /internal/apm/services/{serviceName}/errors/distribution'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + 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; + } + + describe('Error Distributions', () => { + describe('when data is not loaded', () => { + 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); + }); + }); + + describe('when data is loaded', () => { + describe('errors distribution', () => { + const { appleTransaction, bananaTransaction } = config; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); + }); + + after(() => apmSynthtraceEsClient.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 correct start in errors distribution chart', () => { + let errorsDistributionWithComparison: ErrorsDistribution; + before(async () => { + const responseWithComparison = await callApi({ + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + offset: '15m', + }, + }); + errorsDistributionWithComparison = responseWithComparison.body; + }); + it('has same start time when comparison is enabled', () => { + expect(first(errorsDistribution.currentPeriod)?.x).to.equal( + first(errorsDistributionWithComparison.currentPeriod)?.x + ); + }); + }); + }); + + 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', () => { + describe('when data is returned', () => { + 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(), + offset: '5m', + }, + }); + 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 + ); + }); + }); + + describe('when no data is returned', () => { + let errorsDistribution: ErrorsDistribution; + before(async () => { + const response = await callApi({ + query: { + start: '2021-01-03T00:00:00.000Z', + end: '2021-01-03T00:15:00.000Z', + offset: '1d', + }, + }); + errorsDistribution = response.body; + }); + + 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 + ); + }); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/error_group_list.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/error_group_list.spec.ts new file mode 100644 index 0000000000000..c99cbfd3df280 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/error_group_list.spec.ts @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import type { + APIClientRequestParamsOf, + APIReturnType, +} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +type ErrorGroups = + APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['errorGroups']; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + 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; + + const callApi = async ( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['params'] + > + ) => { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics', + params: { + path: { serviceName, ...overrides?.path }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + ...overrides?.query, + }, + }, + }); + }; + describe('Error Group List', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await callApi(); + expect(response.status).to.be(200); + expect(response.body.errorGroups).to.empty(); + }); + }); + + describe('when data is loaded', () => { + describe('errors group', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + const appleTransaction = { + name: 'GET /apple 🍎 ', + successRate: 75, + failureRate: 25, + }; + + const bananaTransaction = { + name: 'GET /banana 🍌', + successRate: 50, + failureRate: 50, + }; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + const serviceInstance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance('instance-a'); + + await apmSynthtraceEsClient.index([ + timerange(start, end) + .interval('1m') + .rate(appleTransaction.successRate) + .generator((timestamp) => + serviceInstance + .transaction({ transactionName: appleTransaction.name }) + .timestamp(timestamp) + .duration(1000) + .success() + ), + timerange(start, end) + .interval('1m') + .rate(appleTransaction.failureRate) + .generator((timestamp) => + serviceInstance + .transaction({ transactionName: appleTransaction.name }) + .errors( + serviceInstance.error({ message: 'error 1', type: 'foo' }).timestamp(timestamp) + ) + .duration(1000) + .timestamp(timestamp) + .failure() + ), + timerange(start, end) + .interval('1m') + .rate(bananaTransaction.successRate) + .generator((timestamp) => + serviceInstance + .transaction({ transactionName: bananaTransaction.name }) + .timestamp(timestamp) + .duration(1000) + .success() + ), + timerange(start, end) + .interval('1m') + .rate(bananaTransaction.failureRate) + .generator((timestamp) => + serviceInstance + .transaction({ transactionName: bananaTransaction.name }) + .errors( + serviceInstance.error({ message: 'error 2', type: 'bar' }).timestamp(timestamp) + ) + .duration(1000) + .timestamp(timestamp) + .failure() + ), + ]); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('returns the correct data', () => { + let errorGroups: ErrorGroups; + before(async () => { + const response = await callApi(); + errorGroups = response.body.errorGroups; + }); + + it('returns correct number of errors', () => { + expect(errorGroups.length).to.equal(2); + expect(errorGroups.map((error) => error.name).sort()).to.eql(['error 1', 'error 2']); + }); + + it('returns correct occurences', () => { + const numberOfBuckets = 15; + expect(errorGroups.map((error) => error.occurrences).sort()).to.eql([ + appleTransaction.failureRate * numberOfBuckets, + bananaTransaction.failureRate * numberOfBuckets, + ]); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/errors/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/generate_data.ts similarity index 69% rename from x-pack/test/apm_api_integration/tests/errors/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/generate_data.ts index a7e627a048e05..ea22c866bd668 100644 --- a/x-pack/test/apm_api_integration/tests/errors/generate_data.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/generate_data.ts @@ -9,7 +9,7 @@ import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; export const config = { appleTransaction: { - name: 'GET /apple 🍎 ', + name: 'GET /apple 🍎', successRate: 75, failureRate: 25, }, @@ -35,14 +35,12 @@ export async function generateData({ .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); - const interval = '1m'; - const { bananaTransaction, appleTransaction } = config; + const interval = timerange(start, end).interval('1m'); const documents = [appleTransaction, bananaTransaction].flatMap((transaction, index) => { return [ - timerange(start, end) - .interval(interval) + interval .rate(transaction.successRate) .generator((timestamp) => serviceGoProdInstance @@ -51,21 +49,18 @@ export async function generateData({ .duration(1000) .success() ), - timerange(start, end) - .interval(interval) - .rate(transaction.failureRate) - .generator((timestamp) => - serviceGoProdInstance - .transaction({ transactionName: transaction.name }) - .errors( - serviceGoProdInstance - .error({ message: `Error ${index}`, type: transaction.name }) - .timestamp(timestamp) - ) - .duration(1000) - .timestamp(timestamp) - .failure() - ), + interval.rate(transaction.failureRate).generator((timestamp) => + serviceGoProdInstance + .transaction({ transactionName: transaction.name }) + .errors( + serviceGoProdInstance + .error({ message: `Error ${index}`, type: transaction.name }) + .timestamp(timestamp) + ) + .duration(1000) + .timestamp(timestamp) + .failure() + ), ]; }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/group_id_samples.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/group_id_samples.spec.ts new file mode 100644 index 0000000000000..9c20c97fde868 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/group_id_samples.spec.ts @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { timerange } from '@kbn/apm-synthtrace-client'; +import { service } from '@kbn/apm-synthtrace-client/src/lib/apm/service'; +import { orderBy } from 'lodash'; +import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { config, generateData } from './generate_data'; + +type ErrorGroupSamples = + APIReturnType<'GET /internal/apm/services/{serviceName}/errors/{groupId}/samples'>; + +type ErrorSampleDetails = + APIReturnType<'GET /internal/apm/services/{serviceName}/errors/{groupId}/error/{errorId}'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + 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 callErrorGroupSamplesApi({ groupId }: { groupId: string }) { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/errors/{groupId}/samples', + params: { + path: { + serviceName, + groupId, + }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }, + }); + return response; + } + + async function callErrorSampleDetailsApi(errorId: string) { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/errors/{groupId}/error/{errorId}', + params: { + path: { + serviceName, + groupId: 'foo', + errorId, + }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }, + }); + return response; + } + describe('Error Group Id Samples', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + }); + + it('handles the empty state', async () => { + const response = await callErrorGroupSamplesApi({ groupId: 'foo' }); + expect(response.status).to.be(200); + expect(response.body.occurrencesCount).to.be(0); + }); + + describe('when samples data is loaded', () => { + let errorsSamplesResponse: ErrorGroupSamples; + const { bananaTransaction } = config; + + describe('error group id', () => { + before(async () => { + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); + const response = await callErrorGroupSamplesApi({ + groupId: '0000000000000000000000000Error 1', + }); + errorsSamplesResponse = response.body; + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('displays correct number of occurrences', () => { + const numberOfBuckets = 15; + expect(errorsSamplesResponse.occurrencesCount).to.equal( + bananaTransaction.failureRate * numberOfBuckets + ); + }); + }); + }); + + describe('when error sample data is loaded', () => { + describe('error sample id', () => { + before(async () => { + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('return correct data', () => { + let errorSampleDetailsResponse: ErrorSampleDetails; + before(async () => { + const errorsSamplesResponse = await callErrorGroupSamplesApi({ + groupId: '0000000000000000000000000Error 1', + }); + + const errorId = errorsSamplesResponse.body.errorSampleIds[0]; + + const response = await callErrorSampleDetailsApi(errorId); + errorSampleDetailsResponse = response.body; + }); + + it('displays correct error grouping_key', () => { + expect(errorSampleDetailsResponse.error.error.grouping_key).to.equal( + '0000000000000000000000000Error 1' + ); + }); + + it('displays correct error message', () => { + expect(errorSampleDetailsResponse.error.error.exception?.[0].message).to.equal( + 'Error 1' + ); + }); + }); + }); + + describe('with sampled and unsampled transactions', () => { + let errorGroupSamplesResponse: ErrorGroupSamples; + + before(async () => { + const instance = service(serviceName, 'production', 'go').instance('a'); + const errorMessage = 'Error 1'; + const groupId = '0000000000000000000000000Error 1'; + + await apmSynthtraceEsClient.index( + timerange(start, end) + .interval('15m') + .rate(1) + .generator((timestamp) => { + return [ + instance + .transaction('GET /api/foo') + .duration(100) + .timestamp(timestamp) + .sample(false) + .errors( + instance.error({ message: errorMessage }).timestamp(timestamp), + instance.error({ message: errorMessage }).timestamp(timestamp + 1) + ), + instance + .transaction('GET /api/foo') + .duration(100) + .timestamp(timestamp) + .sample(true) + .errors(instance.error({ message: errorMessage }).timestamp(timestamp)), + ]; + }) + ); + + errorGroupSamplesResponse = (await callErrorGroupSamplesApi({ groupId })).body; + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('returns the errors in the correct order (sampled first, then unsampled)', () => { + const idsOfErrors = errorGroupSamplesResponse.errorSampleIds.map((id) => + parseInt(id, 10) + ); + + // this checks whether the order of indexing is different from the order that is returned + // if it is not, scoring/sorting is broken + expect(errorGroupSamplesResponse.errorSampleIds.length).to.be(3); + expect(idsOfErrors).to.not.eql(orderBy(idsOfErrors)); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/index.ts new file mode 100644 index 0000000000000..4061edd217462 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('errors', () => { + loadTestFile(require.resolve('./error_group_list.spec.ts')); + loadTestFile(require.resolve('./group_id_samples.spec.ts')); + loadTestFile(require.resolve('./distribution.spec.ts')); + loadTestFile(require.resolve('./top_errors_for_transaction/top_errors_main_stats.spec.ts')); + loadTestFile( + require.resolve('./top_erroneous_transactions/top_erroneous_transactions.spec.ts') + ); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_erroneous_transactions/generate_data.ts similarity index 54% rename from x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_erroneous_transactions/generate_data.ts index 7732d85efa58f..0a2e690ccf966 100644 --- a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/generate_data.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_erroneous_transactions/generate_data.ts @@ -8,15 +8,15 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; export const config = { - firstTransaction: { - name: 'GET /apple 🍎 ', - successRate: 75, - failureRate: 25, + appleTransaction: { + name: 'GET /apple 🍎', + successRate: 25, + failureRate: 75, }, - secondTransaction: { + bananaTransaction: { name: 'GET /banana 🍌', - successRate: 50, - failureRate: 50, + successRate: 80, + failureRate: 20, }, }; @@ -35,14 +35,12 @@ export async function generateData({ .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); - const interval = '1m'; + const { bananaTransaction, appleTransaction } = config; + const interval = timerange(start, end).interval('1m'); - const { firstTransaction, secondTransaction } = config; - - const documents = [firstTransaction, secondTransaction].flatMap((transaction) => { + const documents = [appleTransaction, bananaTransaction].flatMap((transaction, index) => { return [ - timerange(start, end) - .interval(interval) + interval .rate(transaction.successRate) .generator((timestamp) => serviceGoProdInstance @@ -51,21 +49,21 @@ export async function generateData({ .duration(1000) .success() ), - timerange(start, end) - .interval(interval) - .rate(transaction.failureRate) - .generator((timestamp) => - serviceGoProdInstance - .transaction({ transactionName: transaction.name }) - .errors( - serviceGoProdInstance - .error({ message: 'Error 1', type: transaction.name }) - .timestamp(timestamp) - ) - .duration(1000) - .timestamp(timestamp) - .failure() - ), + interval.rate(transaction.failureRate).generator((timestamp) => + serviceGoProdInstance + .transaction({ transactionName: transaction.name }) + .errors( + serviceGoProdInstance + .error({ message: `Error 1`, type: transaction.name }) + .timestamp(timestamp), + serviceGoProdInstance + .error({ message: `Error 2`, type: transaction.name }) + .timestamp(timestamp) + ) + .duration(1000) + .timestamp(timestamp) + .failure() + ), ]; }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts new file mode 100644 index 0000000000000..1fb3cdadd8f67 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts @@ -0,0 +1,211 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { + APIClientRequestParamsOf, + APIReturnType, +} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import { sumBy, first, last } from 'lodash'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import { config, generateData } from './generate_data'; +import { isFiniteNumber } from '../../utils/common'; + +type ErroneousTransactions = + APIReturnType<'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + 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; + const groupId = '0000000000000000000000000Error 1'; + + async function callApi( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions'>['params'] + > + ) { + const response = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions', + params: { + path: { + serviceName, + groupId, + ...overrides?.path, + }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + offset: undefined, + numBuckets: 15, + ...overrides?.query, + }, + }, + }); + return response; + } + + describe('Top erroneous transactions', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + }); + + it('handles the empty state', async () => { + const response = await callApi(); + expect(response.status).to.be(200); + expect(response.body.topErroneousTransactions).to.be.empty(); + }); + + describe('when data is loaded', () => { + const { + appleTransaction: { name: appleTransactionName, failureRate: appleTransactionFailureRate }, + bananaTransaction: { + name: bananaTransactionName, + failureRate: bananaTransactionFailureRate, + }, + } = config; + + describe('returns the correct data', () => { + describe('without comparison', () => { + const numberOfBuckets = 15; + let erroneousTransactions: ErroneousTransactions; + + before(async () => { + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); + const response = await callApi({ + path: { groupId }, + }); + erroneousTransactions = response.body; + }); + + it('displays the correct number of occurrences', () => { + const { topErroneousTransactions } = erroneousTransactions; + expect(topErroneousTransactions.length).to.be(2); + + const bananaTransaction = topErroneousTransactions.find( + (x) => x.transactionName === bananaTransactionName + ); + expect(bananaTransaction).to.not.be(undefined); + expect(bananaTransaction?.occurrences).to.be( + bananaTransactionFailureRate * numberOfBuckets + ); + + const appleTransaction = topErroneousTransactions.find( + (x) => x.transactionName === appleTransactionName + ); + expect(appleTransaction).to.not.be(undefined); + expect(appleTransaction?.occurrences).to.be( + appleTransactionFailureRate * numberOfBuckets + ); + }); + + it('displays the correct number of occurrences in time series', () => { + const { topErroneousTransactions } = erroneousTransactions; + + const bananaTransaction = topErroneousTransactions.find( + (x) => x.transactionName === bananaTransactionName + ); + const firstErrorCount = sumBy(bananaTransaction?.currentPeriodTimeseries, 'y'); + expect(firstErrorCount).to.be(bananaTransactionFailureRate * numberOfBuckets); + + const appleTransaction = topErroneousTransactions.find( + (x) => x.transactionName === appleTransactionName + ); + const secondErrorCount = sumBy(appleTransaction?.currentPeriodTimeseries, 'y'); + expect(secondErrorCount).to.be(appleTransactionFailureRate * numberOfBuckets); + }); + }); + + describe('with comparison', () => { + describe('when there are data for the time periods', () => { + let erroneousTransactions: ErroneousTransactions; + + before(async () => { + const fiveMinutes = 5 * 60 * 1000; + const response = await callApi({ + path: { groupId }, + query: { + start: new Date(end - fiveMinutes).toISOString(), + end: new Date(end).toISOString(), + offset: '5m', + }, + }); + erroneousTransactions = response.body; + }); + + it('returns some data', () => { + const { topErroneousTransactions } = erroneousTransactions; + + const hasCurrentPeriodData = + topErroneousTransactions[0]?.currentPeriodTimeseries.some(({ y }) => + isFiniteNumber(y) + ); + + const hasPreviousPeriodData = + topErroneousTransactions[0]?.previousPeriodTimeseries.some(({ y }) => + isFiniteNumber(y) + ); + + expect(hasCurrentPeriodData).to.be(true); + expect(hasPreviousPeriodData).to.be(true); + }); + + it('has the same start time for both periods', () => { + const { topErroneousTransactions } = erroneousTransactions; + expect(first(topErroneousTransactions[0]?.currentPeriodTimeseries)?.x).to.be( + first(topErroneousTransactions[0]?.previousPeriodTimeseries)?.x + ); + }); + + it('has same end time for both periods', () => { + const { topErroneousTransactions } = erroneousTransactions; + expect(last(topErroneousTransactions[0]?.currentPeriodTimeseries)?.x).to.be( + last(topErroneousTransactions[0]?.previousPeriodTimeseries)?.x + ); + }); + + it('returns same number of buckets for both periods', () => { + const { topErroneousTransactions } = erroneousTransactions; + expect(topErroneousTransactions[0]?.currentPeriodTimeseries.length).to.be( + topErroneousTransactions[0]?.previousPeriodTimeseries.length + ); + }); + }); + + describe('when there are no data for the time period', () => { + it('returns an empty array', async () => { + const response = await callApi({ + path: { groupId }, + query: { + start: '2021-01-03T00:00:00.000Z', + end: '2021-01-03T00:15:00.000Z', + offset: '1d', + }, + }); + + const { + body: { topErroneousTransactions }, + } = response; + + expect(topErroneousTransactions).to.be.empty(); + }); + }); + }); + }); + }); + + after(() => apmSynthtraceEsClient.clean()); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_errors_for_transaction/generate_data.ts similarity index 58% rename from x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_errors_for_transaction/generate_data.ts index 9f983fbb8877b..0259c70b36448 100644 --- a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/generate_data.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_errors_for_transaction/generate_data.ts @@ -8,12 +8,12 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; export const config = { - firstTransaction: { + appleTransaction: { name: 'GET /apple 🍎', successRate: 75, failureRate: 25, }, - secondTransaction: { + bananaTransaction: { name: 'GET /banana 🍌', successRate: 50, failureRate: 50, @@ -35,14 +35,12 @@ export async function generateData({ .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); - const interval = '1m'; + const { appleTransaction, bananaTransaction } = config; + const interval = timerange(start, end).interval('1m'); - const { firstTransaction, secondTransaction } = config; - - const documents = [firstTransaction, secondTransaction].flatMap((transaction, index) => { + const documents = [appleTransaction, bananaTransaction].flatMap((transaction, index) => { return [ - timerange(start, end) - .interval(interval) + interval .rate(transaction.successRate) .generator((timestamp) => serviceGoProdInstance @@ -51,24 +49,21 @@ export async function generateData({ .duration(1000) .success() ), - timerange(start, end) - .interval(interval) - .rate(transaction.failureRate) - .generator((timestamp) => - serviceGoProdInstance - .transaction({ transactionName: transaction.name }) - .errors( - serviceGoProdInstance - .error({ message: `Error 1 transaction ${transaction.name}` }) - .timestamp(timestamp), - serviceGoProdInstance - .error({ message: `Error 2 transaction ${transaction.name}` }) - .timestamp(timestamp) - ) - .duration(1000) - .timestamp(timestamp) - .failure() - ), + interval.rate(transaction.failureRate).generator((timestamp) => + serviceGoProdInstance + .transaction({ transactionName: transaction.name }) + .errors( + serviceGoProdInstance + .error({ message: `Error 1 transaction ${transaction.name}` }) + .timestamp(timestamp), + serviceGoProdInstance + .error({ message: `Error 2 transaction ${transaction.name}` }) + .timestamp(timestamp) + ) + .duration(1000) + .timestamp(timestamp) + .failure() + ), ]; }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts new file mode 100644 index 0000000000000..10088af2061d5 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import type { + APIClientRequestParamsOf, + APIReturnType, +} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import moment from 'moment'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import { config, generateData } from './generate_data'; + +type ErrorGroups = + APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name'>['errorGroups']; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + 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/groups/main_statistics_by_transaction_name'>['params'] + > + ) { + return await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name', + params: { + path: { serviceName, ...overrides?.path }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + maxNumberOfErrorGroups: 5, + transactionType: 'request', + transactionName: overrides?.query?.transactionName ?? '', + ...overrides?.query, + }, + }, + }); + } + + describe('Top Errors main stats', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + }); + + it('handles empty state', async () => { + const response = await callApi(); + expect(response.status).to.be(200); + expect(response.body.errorGroups).to.empty(); + }); + + describe('when data is loaded', () => { + let errorGroups: ErrorGroups; + const { + appleTransaction: { name: appleTransactionName, failureRate: appleTransactionFailureRate }, + } = config; + describe('top errors for transaction', () => { + before(async () => { + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); + const response = await callApi({ query: { transactionName: appleTransactionName } }); + errorGroups = response.body.errorGroups; + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('returns the correct data', () => { + const NUMBER_OF_BUCKETS = 15; + + it('returns correct number of errors', () => { + expect(errorGroups.length).to.equal(2); + }); + + it('error 1 is correct', () => { + const firstError = errorGroups[0]; + expect(firstError).to.not.be(undefined); + expect(firstError?.name).to.be(`Error 1 transaction GET /apple 🍎`); + expect(firstError?.occurrences).to.be(appleTransactionFailureRate * NUMBER_OF_BUCKETS); + expect(firstError?.lastSeen).to.be(moment(end).startOf('minute').valueOf()); + }); + + it('error 2 is correct', () => { + const secondError = errorGroups[1]; + expect(secondError).to.not.be(undefined); + expect(secondError?.name).to.be(`Error 2 transaction GET /apple 🍎`); + expect(secondError?.occurrences).to.be(appleTransactionFailureRate * NUMBER_OF_BUCKETS); + expect(secondError?.lastSeen).to.be(moment(end).startOf('minute').valueOf()); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index aaf095cfb9425..b7edf2c4a6cb4 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -12,6 +12,7 @@ export default function apmApiIntegrationTests({ }: DeploymentAgnosticFtrProviderContext) { describe('APM', function () { loadTestFile(require.resolve('./agent_explorer')); + loadTestFile(require.resolve('./errors')); loadTestFile(require.resolve('./alerts')); loadTestFile(require.resolve('./mobile')); loadTestFile(require.resolve('./custom_dashboards')); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/utils/common.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/utils/common.ts index 8f5ff9822e22d..f115945a4ab52 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/utils/common.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/utils/common.ts @@ -5,8 +5,13 @@ * 2.0. */ -import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; -import { Maybe } from '@kbn/apm-plugin/typings/common'; +import { isFinite } from 'lodash'; +import type { Maybe } from '@kbn/apm-plugin/typings/common'; + +// _.isNumber() returns true for NaN, _.isFinite() does not refine +export function isFiniteNumber(value: any): value is number { + return isFinite(value); +} export function roundNumber(num: Maybe) { return isFiniteNumber(num) ? Number(num.toPrecision(4)) : null; diff --git a/x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts b/x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts deleted file mode 100644 index 544ca97817af0..0000000000000 --- a/x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import expect from '@kbn/expect'; -import { first, last, sumBy } from 'lodash'; -import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; -import { - APIClientRequestParamsOf, - APIReturnType, -} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { config, generateData } from './generate_data'; - -type ErrorsDistribution = - APIReturnType<'GET /internal/apm/services/{serviceName}/errors/distribution'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - 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); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177336 - registry.when('when data is loaded', { config: 'basic', archives: [] }, () => { - describe('errors distribution', () => { - const { appleTransaction, bananaTransaction } = config; - before(async () => { - await generateData({ serviceName, start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.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 correct start in errors distribution chart', () => { - let errorsDistributionWithComparison: ErrorsDistribution; - before(async () => { - const responseWithComparison = await callApi({ - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - offset: '15m', - }, - }); - errorsDistributionWithComparison = responseWithComparison.body; - }); - it('has same start time when comparison is enabled', () => { - expect(first(errorsDistribution.currentPeriod)?.x).to.equal( - first(errorsDistributionWithComparison.currentPeriod)?.x - ); - }); - }); - }); - - 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', () => { - describe('when data is returned', () => { - 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(), - offset: '5m', - }, - }); - 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 - ); - }); - }); - - describe('when no data is returned', () => { - let errorsDistribution: ErrorsDistribution; - before(async () => { - const response = await callApi({ - query: { - start: '2021-01-03T00:00:00.000Z', - end: '2021-01-03T00:15:00.000Z', - offset: '1d', - }, - }); - errorsDistribution = response.body; - }); - - 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 - ); - }); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts b/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts deleted file mode 100644 index fd01833cb4f50..0000000000000 --- a/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import expect from '@kbn/expect'; -import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { - APIClientRequestParamsOf, - APIReturnType, -} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -type ErrorGroups = - APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['errorGroups']; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - 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/groups/main_statistics'>['params'] - > - ) { - return await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics', - params: { - path: { serviceName, ...overrides?.path }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - ...overrides?.query, - }, - }, - }); - } - - registry.when('when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles empty state', async () => { - const response = await callApi(); - expect(response.status).to.be(200); - expect(response.body.errorGroups).to.empty(); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177382 - registry.when('when data is loaded', { config: 'basic', archives: [] }, () => { - describe('errors group', () => { - const appleTransaction = { - name: 'GET /apple 🍎 ', - successRate: 75, - failureRate: 25, - }; - - const bananaTransaction = { - name: 'GET /banana 🍌', - successRate: 50, - failureRate: 50, - }; - - before(async () => { - const serviceInstance = apm - .service({ name: serviceName, environment: 'production', agentName: 'go' }) - .instance('instance-a'); - - await apmSynthtraceEsClient.index([ - timerange(start, end) - .interval('1m') - .rate(appleTransaction.successRate) - .generator((timestamp) => - serviceInstance - .transaction({ transactionName: appleTransaction.name }) - .timestamp(timestamp) - .duration(1000) - .success() - ), - timerange(start, end) - .interval('1m') - .rate(appleTransaction.failureRate) - .generator((timestamp) => - serviceInstance - .transaction({ transactionName: appleTransaction.name }) - .errors( - serviceInstance.error({ message: 'error 1', type: 'foo' }).timestamp(timestamp) - ) - .duration(1000) - .timestamp(timestamp) - .failure() - ), - timerange(start, end) - .interval('1m') - .rate(bananaTransaction.successRate) - .generator((timestamp) => - serviceInstance - .transaction({ transactionName: bananaTransaction.name }) - .timestamp(timestamp) - .duration(1000) - .success() - ), - timerange(start, end) - .interval('1m') - .rate(bananaTransaction.failureRate) - .generator((timestamp) => - serviceInstance - .transaction({ transactionName: bananaTransaction.name }) - .errors( - serviceInstance.error({ message: 'error 2', type: 'bar' }).timestamp(timestamp) - ) - .duration(1000) - .timestamp(timestamp) - .failure() - ), - ]); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('returns the correct data', () => { - let errorGroups: ErrorGroups; - before(async () => { - const response = await callApi(); - errorGroups = response.body.errorGroups; - }); - - it('returns correct number of errors', () => { - expect(errorGroups.length).to.equal(2); - expect(errorGroups.map((error) => error.name).sort()).to.eql(['error 1', 'error 2']); - }); - - it('returns correct occurences', () => { - const numberOfBuckets = 15; - expect(errorGroups.map((error) => error.occurrences).sort()).to.eql([ - appleTransaction.failureRate * numberOfBuckets, - bananaTransaction.failureRate * numberOfBuckets, - ]); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/errors/group_id_samples.spec.ts b/x-pack/test/apm_api_integration/tests/errors/group_id_samples.spec.ts deleted file mode 100644 index ea74f1fa622d8..0000000000000 --- a/x-pack/test/apm_api_integration/tests/errors/group_id_samples.spec.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import expect from '@kbn/expect'; -import { timerange } from '@kbn/apm-synthtrace-client'; -import { service } from '@kbn/apm-synthtrace-client/src/lib/apm/service'; -import { orderBy } from 'lodash'; -import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { getErrorGroupingKey } from '@kbn/apm-synthtrace-client/src/lib/apm/instance'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { config, generateData } from './generate_data'; - -type ErrorGroupSamples = - APIReturnType<'GET /internal/apm/services/{serviceName}/errors/{groupId}/samples'>; - -type ErrorSampleDetails = - APIReturnType<'GET /internal/apm/services/{serviceName}/errors/{groupId}/error/{errorId}'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - 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 callErrorGroupSamplesApi({ groupId }: { groupId: string }) { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/errors/{groupId}/samples', - params: { - path: { - serviceName, - groupId, - }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }, - }); - return response; - } - - async function callErrorSampleDetailsApi(errorId: string) { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/errors/{groupId}/error/{errorId}', - params: { - path: { - serviceName, - groupId: 'foo', - errorId, - }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }, - }); - return response; - } - - registry.when('when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await callErrorGroupSamplesApi({ groupId: 'foo' }); - expect(response.status).to.be(200); - expect(response.body.occurrencesCount).to.be(0); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177397 - registry.when.skip('when samples data is loaded', { config: 'basic', archives: [] }, () => { - const { bananaTransaction } = config; - describe('error group id', () => { - before(async () => { - await generateData({ serviceName, start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('return correct data', () => { - let errorsSamplesResponse: ErrorGroupSamples; - before(async () => { - const response = await callErrorGroupSamplesApi({ - groupId: '98b75903135eac35ad42419bd3b45cf8b4270c61cbd0ede0f7e8c8a9ac9fdb03', - }); - errorsSamplesResponse = response.body; - }); - - it('displays correct number of occurrences', () => { - const numberOfBuckets = 15; - expect(errorsSamplesResponse.occurrencesCount).to.equal( - bananaTransaction.failureRate * numberOfBuckets - ); - }); - }); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177383 - registry.when.skip('when error sample data is loaded', { config: 'basic', archives: [] }, () => { - describe('error sample id', () => { - before(async () => { - await generateData({ serviceName, start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('return correct data', () => { - let errorSampleDetailsResponse: ErrorSampleDetails; - before(async () => { - const errorsSamplesResponse = await callErrorGroupSamplesApi({ - groupId: '98b75903135eac35ad42419bd3b45cf8b4270c61cbd0ede0f7e8c8a9ac9fdb03', - }); - - const errorId = errorsSamplesResponse.body.errorSampleIds[0]; - - const response = await callErrorSampleDetailsApi(errorId); - errorSampleDetailsResponse = response.body; - }); - - it('displays correct error grouping_key', () => { - expect(errorSampleDetailsResponse.error.error.grouping_key).to.equal( - '98b75903135eac35ad42419bd3b45cf8b4270c61cbd0ede0f7e8c8a9ac9fdb03' - ); - }); - - it('displays correct error message', () => { - expect(errorSampleDetailsResponse.error.error.exception?.[0].message).to.equal('Error 1'); - }); - }); - }); - - describe('with sampled and unsampled transactions', () => { - let errorGroupSamplesResponse: ErrorGroupSamples; - - before(async () => { - const instance = service(serviceName, 'production', 'go').instance('a'); - const errorMessage = 'Error 1'; - const groupId = getErrorGroupingKey(errorMessage); - - await apmSynthtraceEsClient.index([ - timerange(start, end) - .interval('15m') - .rate(1) - .generator((timestamp) => { - return [ - instance - .transaction('GET /api/foo') - .duration(100) - .timestamp(timestamp) - .sample(false) - .errors( - instance.error({ message: errorMessage }).timestamp(timestamp), - instance.error({ message: errorMessage }).timestamp(timestamp + 1) - ), - instance - .transaction('GET /api/foo') - .duration(100) - .timestamp(timestamp) - .sample(true) - .errors(instance.error({ message: errorMessage }).timestamp(timestamp)), - ]; - }), - ]); - - errorGroupSamplesResponse = (await callErrorGroupSamplesApi({ groupId })).body; - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('returns the errors in the correct order (sampled first, then unsampled)', () => { - const idsOfErrors = errorGroupSamplesResponse.errorSampleIds.map((id) => parseInt(id, 10)); - - // this checks whether the order of indexing is different from the order that is returned - // if it is not, scoring/sorting is broken - expect(errorGroupSamplesResponse.errorSampleIds.length).to.be(3); - expect(idsOfErrors).to.not.eql(orderBy(idsOfErrors)); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts b/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts deleted file mode 100644 index 53b305f093ce4..0000000000000 --- a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import expect from '@kbn/expect'; -import { - APIClientRequestParamsOf, - APIReturnType, -} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import { sumBy, first, last } from 'lodash'; -import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { config, generateData } from './generate_data'; - -type ErroneousTransactions = - APIReturnType<'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - 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; - const groupId = '98b75903135eac35ad42419bd3b45cf8b4270c61cbd0ede0f7e8c8a9ac9fdb03'; - - async function callApi( - overrides?: RecursivePartial< - APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions'>['params'] - > - ) { - const response = await apmApiClient.readUser({ - endpoint: - 'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions', - params: { - path: { - serviceName, - groupId: 'test', - ...overrides?.path, - }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - offset: undefined, - numBuckets: 15, - ...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.topErroneousTransactions).to.be.empty(); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177637 - registry.when.skip('when data is loaded', { config: 'basic', archives: [] }, () => { - const { - firstTransaction: { name: firstTransactionName, failureRate: firstTransactionFailureRate }, - secondTransaction: { name: secondTransactionName, failureRate: secondTransactionFailureRate }, - } = config; - - describe('returns the correct data', () => { - before(async () => { - await generateData({ serviceName, start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('without comparison', () => { - const numberOfBuckets = 15; - let erroneousTransactions: ErroneousTransactions; - - before(async () => { - const response = await callApi({ - path: { groupId }, - }); - erroneousTransactions = response.body; - }); - - it.skip('displays the correct number of occurrences', () => { - const { topErroneousTransactions } = erroneousTransactions; - expect(topErroneousTransactions.length).to.be(2); - - const firstTransaction = topErroneousTransactions.find( - (x) => x.transactionName === firstTransactionName - ); - expect(firstTransaction).to.not.be(undefined); - expect(firstTransaction?.occurrences).to.be( - firstTransactionFailureRate * numberOfBuckets - ); - - const secondTransaction = topErroneousTransactions.find( - (x) => x.transactionName === secondTransactionName - ); - expect(secondTransaction).to.not.be(undefined); - expect(secondTransaction?.occurrences).to.be( - secondTransactionFailureRate * numberOfBuckets - ); - }); - - it('displays the correct number of occurrences in time series', () => { - const { topErroneousTransactions } = erroneousTransactions; - - const firstTransaction = topErroneousTransactions.find( - (x) => x.transactionName === firstTransactionName - ); - const firstErrorCount = sumBy(firstTransaction?.currentPeriodTimeseries, 'y'); - expect(firstErrorCount).to.be(firstTransactionFailureRate * numberOfBuckets); - - const secondTransaction = topErroneousTransactions.find( - (x) => x.transactionName === secondTransactionName - ); - const secondErrorCount = sumBy(secondTransaction?.currentPeriodTimeseries, 'y'); - expect(secondErrorCount).to.be(secondTransactionFailureRate * numberOfBuckets); - }); - }); - - describe('with comparison', () => { - describe('when there are data for the time periods', () => { - let erroneousTransactions: ErroneousTransactions; - - before(async () => { - const fiveMinutes = 5 * 60 * 1000; - const response = await callApi({ - path: { groupId }, - query: { - start: new Date(end - fiveMinutes).toISOString(), - end: new Date(end).toISOString(), - offset: '5m', - }, - }); - erroneousTransactions = response.body; - }); - - it('returns some data', () => { - const { topErroneousTransactions } = erroneousTransactions; - - const hasCurrentPeriodData = topErroneousTransactions[0].currentPeriodTimeseries.some( - ({ y }) => isFiniteNumber(y) - ); - - const hasPreviousPeriodData = topErroneousTransactions[0].previousPeriodTimeseries.some( - ({ y }) => isFiniteNumber(y) - ); - - expect(hasCurrentPeriodData).to.be(true); - expect(hasPreviousPeriodData).to.be(true); - }); - - it('has the same start time for both periods', () => { - const { topErroneousTransactions } = erroneousTransactions; - expect(first(topErroneousTransactions[0].currentPeriodTimeseries)?.x).to.be( - first(topErroneousTransactions[0].previousPeriodTimeseries)?.x - ); - }); - - it('has same end time for both periods', () => { - const { topErroneousTransactions } = erroneousTransactions; - expect(last(topErroneousTransactions[0].currentPeriodTimeseries)?.x).to.be( - last(topErroneousTransactions[0].previousPeriodTimeseries)?.x - ); - }); - - it('returns same number of buckets for both periods', () => { - const { topErroneousTransactions } = erroneousTransactions; - expect(topErroneousTransactions[0].currentPeriodTimeseries.length).to.be( - topErroneousTransactions[0].previousPeriodTimeseries.length - ); - }); - }); - - describe('when there are no data for the time period', () => { - it('returns an empty array', async () => { - const response = await callApi({ - path: { groupId }, - query: { - start: '2021-01-03T00:00:00.000Z', - end: '2021-01-03T00:15:00.000Z', - offset: '1d', - }, - }); - - const { - body: { topErroneousTransactions }, - } = response; - - expect(topErroneousTransactions).to.be.empty(); - }); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts b/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts deleted file mode 100644 index a6476e76a3918..0000000000000 --- a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import expect from '@kbn/expect'; -import { - APIClientRequestParamsOf, - APIReturnType, -} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import moment from 'moment'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { config, generateData } from './generate_data'; - -type ErrorGroups = - APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name'>['errorGroups']; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - 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/groups/main_statistics_by_transaction_name'>['params'] - > - ) { - return await apmApiClient.readUser({ - endpoint: - 'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name', - params: { - path: { serviceName, ...overrides?.path }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - maxNumberOfErrorGroups: 5, - transactionType: 'request', - transactionName: '', - ...overrides?.query, - }, - }, - }); - } - - registry.when('when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles empty state', async () => { - const response = await callApi(); - expect(response.status).to.be(200); - expect(response.body.errorGroups).to.empty(); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177638 - registry.when.skip('when data is loaded', { config: 'basic', archives: [] }, () => { - describe('top errors for transaction', () => { - const { - firstTransaction: { name: firstTransactionName, failureRate: firstTransactionFailureRate }, - } = config; - - before(async () => { - await generateData({ serviceName, start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('returns the correct data', () => { - const NUMBER_OF_BUCKETS = 15; - let errorGroups: ErrorGroups; - before(async () => { - const response = await callApi({ query: { transactionName: firstTransactionName } }); - errorGroups = response.body.errorGroups; - }); - - it('returns correct number of errors', () => { - expect(errorGroups.length).to.equal(2); - }); - - it('error 1 is correct', () => { - const firstErrorId = `b6c1d4d41b0b60b841f40232497344ba36856fcbea0692a4695562ca73e790bd`; - const firstError = errorGroups.find((x) => x.groupId === firstErrorId); - expect(firstError).to.not.be(undefined); - expect(firstError?.groupId).to.be(firstErrorId); - expect(firstError?.name).to.be(`Error 1 transaction GET /apple 🍎`); - expect(firstError?.occurrences).to.be(firstTransactionFailureRate * NUMBER_OF_BUCKETS); - expect(firstError?.lastSeen).to.be(moment(end).startOf('minute').valueOf()); - }); - - it('error 2 is correct', () => { - const secondErrorId = `c3f388e4f7276d4fab85aa2fad2d2a42e70637f65cd5ec9f085de28b36e69ba5`; - const secondError = errorGroups.find((x) => x.groupId === secondErrorId); - expect(secondError).to.not.be(undefined); - expect(secondError?.groupId).to.be(secondErrorId); - expect(secondError?.name).to.be(`Error 2 transaction GET /apple 🍎`); - expect(secondError?.occurrences).to.be(firstTransactionFailureRate * NUMBER_OF_BUCKETS); - expect(secondError?.lastSeen).to.be(moment(end).startOf('minute').valueOf()); - }); - }); - }); - }); -}