[8.x] [APM] Migrate /mobile API tests to deployment agnostic folder (#199021) (#199689)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[APM] Migrate `/mobile` API tests to deployment agnostic folder
(#199021)](https://github.com/elastic/kibana/pull/199021)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT
[{"author":{"name":"Katerina","email":"aikaterini.patticha@elastic.co"},"sourceCommit":{"committedDate":"2024-11-11T13:49:54Z","message":"[APM]
Migrate `/mobile` API tests to deployment agnostic folder
(#199021)\n\ncloses
https://github.com/elastic/kibana/issues/198980\r\n\r\nIn addition to
migrating the mobile api tests, the PR includes\r\n\r\n- Fixing mapping
issue with `error.grouping_name` which causing to drop\r\ndocuments\r\n-
Fix and unskip mobile tests\r\n\r\n### How to test\r\n\r\n-
Serverless\r\n\r\n```\r\nnode scripts/functional_tests_server --config
x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts\r\nnode
scripts/functional_test_runner --config
x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts
--grep=\"APM\"\r\n```\r\n\r\nIt's recommended to be run
against\r\n[MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki)\r\n\r\n-
Stateful\r\n```\r\nnode scripts/functional_tests_server --config
x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts\r\nnode
scripts/functional_test_runner --config
x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts
--grep=\"APM\"\r\n\r\n```\r\n\r\nTODO \r\n- [x] flaky runner \r\n- [x]
locally pass\r\n- [x] mki run\r\n\r\n---------\r\n\r\nCo-authored-by:
Carlos Crespo
<carloshenrique.leonelcrespo@elastic.co>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"f77a8052f7a3e39cb3b61d1b61dbef8b6c254525","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-infra_services"],"number":199021,"url":"https://github.com/elastic/kibana/pull/199021","mergeCommit":{"message":"[APM]
Migrate `/mobile` API tests to deployment agnostic folder
(#199021)\n\ncloses
https://github.com/elastic/kibana/issues/198980\r\n\r\nIn addition to
migrating the mobile api tests, the PR includes\r\n\r\n- Fixing mapping
issue with `error.grouping_name` which causing to drop\r\ndocuments\r\n-
Fix and unskip mobile tests\r\n\r\n### How to test\r\n\r\n-
Serverless\r\n\r\n```\r\nnode scripts/functional_tests_server --config
x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts\r\nnode
scripts/functional_test_runner --config
x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts
--grep=\"APM\"\r\n```\r\n\r\nIt's recommended to be run
against\r\n[MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki)\r\n\r\n-
Stateful\r\n```\r\nnode scripts/functional_tests_server --config
x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts\r\nnode
scripts/functional_test_runner --config
x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts
--grep=\"APM\"\r\n\r\n```\r\n\r\nTODO \r\n- [x] flaky runner \r\n- [x]
locally pass\r\n- [x] mki run\r\n\r\n---------\r\n\r\nCo-authored-by:
Carlos Crespo
<carloshenrique.leonelcrespo@elastic.co>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"f77a8052f7a3e39cb3b61d1b61dbef8b6c254525"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/199021","number":199021,"mergeCommit":{"message":"[APM]
Migrate `/mobile` API tests to deployment agnostic folder
(#199021)\n\ncloses
https://github.com/elastic/kibana/issues/198980\r\n\r\nIn addition to
migrating the mobile api tests, the PR includes\r\n\r\n- Fixing mapping
issue with `error.grouping_name` which causing to drop\r\ndocuments\r\n-
Fix and unskip mobile tests\r\n\r\n### How to test\r\n\r\n-
Serverless\r\n\r\n```\r\nnode scripts/functional_tests_server --config
x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts\r\nnode
scripts/functional_test_runner --config
x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts
--grep=\"APM\"\r\n```\r\n\r\nIt's recommended to be run
against\r\n[MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki)\r\n\r\n-
Stateful\r\n```\r\nnode scripts/functional_tests_server --config
x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts\r\nnode
scripts/functional_test_runner --config
x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts
--grep=\"APM\"\r\n\r\n```\r\n\r\nTODO \r\n- [x] flaky runner \r\n- [x]
locally pass\r\n- [x] mki run\r\n\r\n---------\r\n\r\nCo-authored-by:
Carlos Crespo
<carloshenrique.leonelcrespo@elastic.co>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"f77a8052f7a3e39cb3b61d1b61dbef8b6c254525"}}]}]
BACKPORT-->
This commit is contained in:
Katerina 2024-11-18 14:55:03 +02:00 committed by GitHub
parent db82fe50c8
commit 900a2def42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1238 additions and 1213 deletions

View file

@ -70,7 +70,6 @@ export class Instance extends Entity<ApmFields> {
...this.fields,
'error.type': 'crash',
'error.exception': [{ message, ...(type ? { type } : {}) }],
'error.grouping_name': getErrorGroupingKey(message),
});
}
error({

View file

@ -262,7 +262,6 @@ export class MobileDevice extends Entity<ApmFields> {
'error.type': 'crash',
'error.id': generateLongIdWithSeed(message),
'error.exception': [{ message, ...{ type: 'crash' } }],
'error.grouping_name': groupingName || message,
});
}
}

View file

@ -23,9 +23,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
it('returns a version when agent is listed in the file', async () => {
const { status, body } = await callApi();
expect(status).to.be(200);
const agents = body.data;
const nodeAgent = agents[nodeAgentName] as ElasticApmAgentLatestVersion;
expect(nodeAgent?.latest_version).not.to.be(undefined);
});

View file

@ -12,6 +12,7 @@ export default function apmApiIntegrationTests({
}: DeploymentAgnosticFtrProviderContext) {
describe('APM', function () {
loadTestFile(require.resolve('./agent_explorer'));
loadTestFile(require.resolve('./mobile'));
loadTestFile(require.resolve('./errors'));
loadTestFile(require.resolve('./alerts'));
loadTestFile(require.resolve('./custom_dashboards'));

View file

@ -0,0 +1,160 @@
/*
* 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 type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context';
type ErrorGroups =
APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/crashes/groups/main_statistics'>['errorGroups'];
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const synthtrace = getService('synthtrace');
const serviceName = 'synth-swift';
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/mobile-services/{serviceName}/crashes/groups/main_statistics'>['params']
>
) {
return await apmApiClient.readUser({
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/crashes/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('Crash group list', () => {
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 () => {
const serviceInstance = apm
.service({ name: serviceName, environment: 'production', agentName: 'swift' })
.instance('instance-a');
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
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
.crash({
message: 'crash 1',
})
.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
.crash({
message: 'crash 2',
})
.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 crashes', () => {
expect(errorGroups.length).to.equal(2);
expect(errorGroups.map((error) => error.name).sort()).to.eql(['crash 1', 'crash 2']);
});
it('returns correct occurrences', () => {
const numberOfBuckets = 15;
expect(errorGroups.map((error) => error.occurrences).sort()).to.eql([
appleTransaction.failureRate * numberOfBuckets,
bananaTransaction.failureRate * numberOfBuckets,
]);
});
});
});
});
});
}

View file

@ -0,0 +1,209 @@
/*
* 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 type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import { RecursivePartial } from '@kbn/apm-plugin/typings/common';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context';
import { config, generateData } from './generate_data';
type ErrorsDistribution =
APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/crashes/distribution'>;
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const synthtrace = getService('synthtrace');
const serviceName = 'synth-swift';
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/mobile-services/{serviceName}/crashes/distribution'>['params']
>
) {
const response = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/crashes/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('Distribution', () => {
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.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;
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
);
});
});
});
});
});
});
}

View file

@ -0,0 +1,191 @@
/*
* 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 { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
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('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
);
});
});
});
// github.com/elastic/kibana/issues/177665
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));
});
});
});
});
}

View file

@ -0,0 +1,25 @@
/*
* 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('Mobile', () => {
loadTestFile(require.resolve('./crashes/crash_group_list.spec.ts'));
loadTestFile(require.resolve('./crashes/distribution.spec.ts'));
loadTestFile(require.resolve('./errors/group_id_samples.spec.ts'));
loadTestFile(require.resolve('./mobile_detailed_statistics_by_field.spec.ts'));
loadTestFile(require.resolve('./mobile_filters.spec.ts'));
loadTestFile(require.resolve('./mobile_http_requests_timeseries.spec.ts'));
loadTestFile(require.resolve('./mobile_location_stats.spec.ts'));
loadTestFile(require.resolve('./mobile_main_statistics_by_field.spec.ts'));
loadTestFile(require.resolve('./mobile_most_used_chart.spec.ts'));
loadTestFile(require.resolve('./mobile_sessions_timeseries.spec.ts'));
loadTestFile(require.resolve('./mobile_stats.spec.ts'));
loadTestFile(require.resolve('./mobile_terms_by_field.spec.ts'));
});
}

View file

@ -10,16 +10,18 @@ import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_value
import { isEmpty } from 'lodash';
import moment from 'moment';
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
import { generateMobileData, SERVICE_VERSIONS } from './generate_mobile_data';
type MobileDetailedStatisticsResponse =
APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/detailed_statistics'>;
export default function ApiTest({ getService }: FtrProviderContext) {
const apmApiClient = getService('apmApiClient');
const registry = getService('registry');
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const synthtrace = getService('synthtrace');
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
const start = new Date('2023-01-01T00:00:00.000Z').getTime();
const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1;
@ -56,27 +58,24 @@ export default function ApiTest({ getService }: FtrProviderContext) {
.then(({ body }) => body);
}
registry.when(
'Mobile detailed statistics when data is not loaded',
{ config: 'basic', archives: [] },
() => {
describe('when no data', () => {
it('handles empty state', async () => {
const response = await getMobileDetailedStatisticsByField({
serviceName: 'foo',
field: 'service.version',
});
expect(response).to.be.eql({ currentPeriod: {}, previousPeriod: {} });
});
});
}
);
describe('Mobile detailed statistics ', () => {
before(async () => {
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
});
// FLAKY: https://github.com/elastic/kibana/issues/177388
registry.when.skip(
'Mobile detailed statistics when data is loaded',
{ config: 'basic', archives: [] },
() => {
after(() => apmSynthtraceEsClient.clean());
describe('when data is not loaded', () => {
it('handles empty state', async () => {
const response = await getMobileDetailedStatisticsByField({
serviceName: 'foo',
field: 'service.version',
});
expect(response).to.be.eql({ currentPeriod: {}, previousPeriod: {} });
});
});
describe('when data is loaded', () => {
before(async () => {
await generateMobileData({
apmSynthtraceEsClient,
@ -85,8 +84,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
});
after(() => apmSynthtraceEsClient.clean());
describe('when comparison is disable', () => {
it('returns current period data only', async () => {
const response = await getMobileDetailedStatisticsByField({
@ -133,6 +130,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
});
});
}
);
});
});
}

View file

@ -7,10 +7,10 @@
import expect from '@kbn/expect';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
type MobileFilters = APIReturnType<'GET /internal/apm/services/{serviceName}/mobile/filters'>;
@ -133,10 +133,11 @@ async function generateData({
]);
}
export default function ApiTest({ getService }: FtrProviderContext) {
const apmApiClient = getService('apmApiClient');
const registry = getService('registry');
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const synthtrace = getService('synthtrace');
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
const start = new Date('2023-01-01T00:00:00.000Z').getTime();
const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1;
@ -166,7 +167,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
.then(({ body }) => body);
}
registry.when('Mobile filters when data is not loaded', { config: 'basic', archives: [] }, () => {
describe('Mobile filters', () => {
describe('when no data', () => {
it('handles empty state', async () => {
const response = await getMobileFilters({ serviceName: 'foo' });
@ -175,30 +176,25 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
});
});
});
// FLAKY: https://github.com/elastic/kibana/issues/177389
registry.when.skip('Mobile filters', { config: 'basic', archives: [] }, () => {
before(async () => {
await generateData({
apmSynthtraceEsClient,
start,
end,
});
});
after(() => apmSynthtraceEsClient.clean());
describe('when data is loaded', () => {
let response: MobileFilters;
before(async () => {
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
await generateData({
apmSynthtraceEsClient,
start,
end,
});
response = await getMobileFilters({
serviceName: 'synth-android',
environment: 'production',
});
});
after(() => apmSynthtraceEsClient.clean());
let response: MobileFilters;
it('returns correct filters for device', () => {
response.mobileFilters.map(({ key, options }) => {
if (key === 'device') {

View file

@ -7,13 +7,15 @@
import expect from '@kbn/expect';
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
import { generateMobileData } from './generate_mobile_data';
export default function ApiTest({ getService }: FtrProviderContext) {
const apmApiClient = getService('apmApiClient');
const registry = getService('registry');
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const synthtrace = getService('synthtrace');
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
const start = new Date('2023-01-01T00:00:00.000Z').getTime();
const end = new Date('2023-01-01T02:00:00.000Z').getTime();
@ -47,27 +49,20 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
}
registry.when.skip(
'Mobile HTTP requests without data loaded',
{ config: 'basic', archives: [] },
() => {
describe('when no data', () => {
it('handles empty state', async () => {
const response = await getHttpRequestsChart({ serviceName: 'foo' });
expect(response.body.currentPeriod.timeseries).to.eql([]);
expect(response.body.previousPeriod.timeseries).to.eql([]);
expect(response.status).to.be(200);
});
describe('Mobile HTTP requests ', () => {
describe('when no data', () => {
it('handles empty state', async () => {
const response = await getHttpRequestsChart({ serviceName: 'foo' });
expect(response.body.currentPeriod.timeseries).to.eql([]);
expect(response.body.previousPeriod.timeseries).to.eql([]);
expect(response.status).to.be(200);
});
}
);
});
// FLAKY: https://github.com/elastic/kibana/issues/177390
registry.when.skip(
'Mobile HTTP requests with data loaded',
{ config: 'basic', archives: [] },
() => {
describe('when data is loaded', () => {
before(async () => {
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
await generateMobileData({
apmSynthtraceEsClient,
start,
@ -76,32 +71,29 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
after(() => apmSynthtraceEsClient.clean());
describe('when data is loaded', () => {
it('returns timeseries for http requests chart', async () => {
const response = await getHttpRequestsChart({
serviceName: 'synth-android',
offset: '1d',
});
expect(response.status).to.be(200);
expect(response.body.currentPeriod.timeseries.some((item) => item.x && item.y)).to.eql(
true
);
expect(response.body.previousPeriod.timeseries[0].y).to.eql(0);
it('returns timeseries for http requests chart', async () => {
const response = await getHttpRequestsChart({
serviceName: 'synth-android',
offset: '1d',
});
it('returns only current period timeseries when offset is not available', async () => {
const response = await getHttpRequestsChart({ serviceName: 'synth-android' });
expect(response.status).to.be(200);
expect(response.body.currentPeriod.timeseries.some((item) => item.x && item.y)).to.eql(
true
);
expect(response.body.previousPeriod.timeseries[0].y).to.eql(0);
});
expect(response.status).to.be(200);
expect(
response.body.currentPeriod.timeseries.some((item) => item.y === 0 && item.x)
).to.eql(true);
it('returns only current period timeseries when offset is not available', async () => {
const response = await getHttpRequestsChart({ serviceName: 'synth-android' });
expect(response.body.currentPeriod.timeseries[0].y).to.eql(7);
expect(response.body.previousPeriod.timeseries).to.eql([]);
});
expect(response.status).to.be(200);
expect(
response.body.currentPeriod.timeseries.some((item) => item.y === 0 && item.x)
).to.eql(true);
expect(response.body.currentPeriod.timeseries[0].y).to.eql(7);
expect(response.body.previousPeriod.timeseries).to.eql([]);
});
describe('when filters are applied', () => {
@ -138,6 +130,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(ntcCell.body.currentPeriod.timeseries[0].y).to.eql(2);
});
});
}
);
});
});
}

View file

@ -7,10 +7,10 @@
import expect from '@kbn/expect';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
type MobileLocationStats =
APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/location/stats'>;
@ -176,10 +176,11 @@ async function generateData({
]);
}
export default function ApiTest({ getService }: FtrProviderContext) {
const apmApiClient = getService('apmApiClient');
const registry = getService('registry');
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const synthtrace = getService('synthtrace');
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
const start = new Date('2023-01-01T00:00:00.000Z').getTime();
const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1;
@ -212,7 +213,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
.then(({ body }) => body);
}
registry.when('Location stats when data is not loaded', { config: 'basic', archives: [] }, () => {
describe('Location stats', () => {
describe('when no data', () => {
it('handles empty state', async () => {
const response = await getMobileLocationStats({ serviceName: 'foo' });
@ -230,111 +231,112 @@ export default function ApiTest({ getService }: FtrProviderContext) {
);
});
});
});
// FLAKY: https://github.com/elastic/kibana/issues/177396
registry.when.skip('Location stats', { config: 'basic', archives: [] }, () => {
before(async () => {
await generateData({
apmSynthtraceEsClient,
start,
end,
});
});
after(() => apmSynthtraceEsClient.clean());
describe('when data is loaded', () => {
let response: MobileLocationStats;
describe('Location stats with data', () => {
before(async () => {
response = await getMobileLocationStats({
serviceName: 'synth-android',
environment: 'production',
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
await generateData({
apmSynthtraceEsClient,
start,
end,
});
});
it('returns location for most sessions', () => {
const { location } = response.currentPeriod.mostSessions;
expect(location).to.be('China');
});
after(() => apmSynthtraceEsClient.clean());
it('returns location for most requests', () => {
const { location } = response.currentPeriod.mostRequests;
expect(location).to.be('China');
});
describe('when data is loaded', () => {
let response: MobileLocationStats;
it('returns location for most crashes', () => {
const { location } = response.currentPeriod.mostCrashes;
expect(location).to.be('China');
});
it('returns location for most launches', () => {
const { location } = response.currentPeriod.mostLaunches;
expect(location).to.be('China');
});
});
describe('when filters are applied', () => {
it('returns empty state for filters with no results', async () => {
const response = await getMobileLocationStats({
serviceName: 'synth-android',
environment: 'production',
kuery: `app.version:"none"`,
before(async () => {
response = await getMobileLocationStats({
serviceName: 'synth-android',
environment: 'production',
});
});
expect(response.currentPeriod.mostSessions.value).to.eql(0);
expect(response.currentPeriod.mostRequests.value).to.eql(0);
expect(response.currentPeriod.mostCrashes.value).to.eql(0);
expect(response.currentPeriod.mostLaunches.value).to.eql(0);
expect(response.currentPeriod.mostSessions.timeseries.every((item) => item.y === 0)).to.eql(
true
);
expect(response.currentPeriod.mostRequests.timeseries.every((item) => item.y === 0)).to.eql(
true
);
expect(response.currentPeriod.mostCrashes.timeseries.every((item) => item.y === 0)).to.eql(
true
);
expect(response.currentPeriod.mostLaunches.timeseries.every((item) => item.y === 0)).to.eql(
true
);
});
it('returns the correct values when single filter is applied', async () => {
const response = await getMobileLocationStats({
serviceName: 'synth-android',
environment: 'production',
kuery: `service.version:"1.1"`,
it('returns location for most sessions', () => {
const { location } = response.currentPeriod.mostSessions;
expect(location).to.be('China');
});
expect(response.currentPeriod.mostSessions.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostCrashes.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostRequests.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostLaunches.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostSessions.value).to.eql(3);
expect(response.currentPeriod.mostRequests.value).to.eql(3);
expect(response.currentPeriod.mostCrashes.value).to.eql(3);
expect(response.currentPeriod.mostLaunches.value).to.eql(3);
});
it('returns the correct values when multiple filters are applied', async () => {
const response = await getMobileLocationStats({
serviceName: 'synth-android',
kuery: `service.version:"1.1" and service.environment: "production"`,
it('returns location for most requests', () => {
const { location } = response.currentPeriod.mostRequests;
expect(location).to.be('China');
});
expect(response.currentPeriod.mostSessions.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostCrashes.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostRequests.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostLaunches.timeseries[0].y).to.eql(1);
it('returns location for most crashes', () => {
const { location } = response.currentPeriod.mostCrashes;
expect(location).to.be('China');
});
expect(response.currentPeriod.mostSessions.value).to.eql(3);
expect(response.currentPeriod.mostRequests.value).to.eql(3);
expect(response.currentPeriod.mostCrashes.value).to.eql(3);
expect(response.currentPeriod.mostLaunches.value).to.eql(3);
it('returns location for most launches', () => {
const { location } = response.currentPeriod.mostLaunches;
expect(location).to.be('China');
});
});
describe('when filters are applied', () => {
it('returns empty state for filters with no results', async () => {
const response = await getMobileLocationStats({
serviceName: 'synth-android',
environment: 'production',
kuery: `app.version:"none"`,
});
expect(response.currentPeriod.mostSessions.value).to.eql(0);
expect(response.currentPeriod.mostRequests.value).to.eql(0);
expect(response.currentPeriod.mostCrashes.value).to.eql(0);
expect(response.currentPeriod.mostLaunches.value).to.eql(0);
expect(
response.currentPeriod.mostSessions.timeseries.every((item) => item.y === 0)
).to.eql(true);
expect(
response.currentPeriod.mostRequests.timeseries.every((item) => item.y === 0)
).to.eql(true);
expect(
response.currentPeriod.mostCrashes.timeseries.every((item) => item.y === 0)
).to.eql(true);
expect(
response.currentPeriod.mostLaunches.timeseries.every((item) => item.y === 0)
).to.eql(true);
});
it('returns the correct values when single filter is applied', async () => {
const response = await getMobileLocationStats({
serviceName: 'synth-android',
environment: 'production',
kuery: `service.version:"1.1"`,
});
expect(response.currentPeriod.mostSessions.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostCrashes.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostRequests.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostLaunches.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostSessions.value).to.eql(3);
expect(response.currentPeriod.mostRequests.value).to.eql(3);
expect(response.currentPeriod.mostCrashes.value).to.eql(3);
expect(response.currentPeriod.mostLaunches.value).to.eql(3);
});
it('returns the correct values when multiple filters are applied', async () => {
const response = await getMobileLocationStats({
serviceName: 'synth-android',
kuery: `service.version:"1.1" and service.environment: "production"`,
});
expect(response.currentPeriod.mostSessions.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostCrashes.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostRequests.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostLaunches.timeseries[0].y).to.eql(1);
expect(response.currentPeriod.mostSessions.value).to.eql(3);
expect(response.currentPeriod.mostRequests.value).to.eql(3);
expect(response.currentPeriod.mostCrashes.value).to.eql(3);
expect(response.currentPeriod.mostLaunches.value).to.eql(3);
});
});
});
});

View file

@ -4,12 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import expect from '@kbn/expect';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
const GALAXY_DURATION = 500;
const HUAWEI_DURATION = 20;
@ -126,10 +125,11 @@ async function generateData({
]);
}
export default function ApiTest({ getService }: FtrProviderContext) {
const apmApiClient = getService('apmApiClient');
const registry = getService('registry');
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const synthtrace = getService('synthtrace');
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
const start = new Date('2023-01-01T00:00:00.000Z').getTime();
const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1;
@ -162,91 +162,88 @@ export default function ApiTest({ getService }: FtrProviderContext) {
.then(({ body }) => body);
}
registry.when(
'Mobile main statistics when data is not loaded',
{ config: 'basic', archives: [] },
() => {
describe('when no data', () => {
it('handles empty state', async () => {
const response = await getMobileMainStatisticsByField({
serviceName: 'foo',
field: 'service.version',
});
expect(response.mainStatistics.length).to.be(0);
describe('Mobile main statistics', () => {
describe('when no data', () => {
it('handles empty state', async () => {
const response = await getMobileMainStatisticsByField({
serviceName: 'foo',
field: 'service.version',
});
});
}
);
// FLAKY: https://github.com/elastic/kibana/issues/177395
registry.when.skip('Mobile main statistics', { config: 'basic', archives: [] }, () => {
before(async () => {
await generateData({
apmSynthtraceEsClient,
start,
end,
expect(response.mainStatistics.length).to.be(0);
});
});
after(() => apmSynthtraceEsClient.clean());
describe('Mobile main statistics', () => {
before(async () => {
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
describe('when data is loaded', () => {
const huaweiLatency = calculateLatency(HUAWEI_DURATION);
const galaxyLatency = calculateLatency(GALAXY_DURATION);
const huaweiThroughput = calculateThroughput({ start, end });
const galaxyThroughput = calculateThroughput({ start, end });
it('returns the correct data for App version', async () => {
const response = await getMobileMainStatisticsByField({
serviceName: 'synth-android',
environment: 'production',
field: 'service.version',
await generateData({
apmSynthtraceEsClient,
start,
end,
});
const fieldValues = response.mainStatistics.map((item) => item.name);
expect(fieldValues).to.be.eql(SERVICE_VERSIONS);
const latencyValues = response.mainStatistics.map((item) => item.latency);
expect(latencyValues).to.be.eql([galaxyLatency, huaweiLatency]);
const throughputValues = response.mainStatistics.map((item) => item.throughput);
expect(throughputValues).to.be.eql([galaxyThroughput, huaweiThroughput]);
});
it('returns the correct data for Os version', async () => {
const response = await getMobileMainStatisticsByField({
serviceName: 'synth-android',
environment: 'production',
field: 'host.os.version',
after(() => apmSynthtraceEsClient.clean());
describe('when data is loaded', () => {
const huaweiLatency = calculateLatency(HUAWEI_DURATION);
const galaxyLatency = calculateLatency(GALAXY_DURATION);
const huaweiThroughput = calculateThroughput({ start, end });
const galaxyThroughput = calculateThroughput({ start, end });
it('returns the correct data for App version', async () => {
const response = await getMobileMainStatisticsByField({
serviceName: 'synth-android',
environment: 'production',
field: 'service.version',
});
const fieldValues = response.mainStatistics.map((item) => item.name);
expect(fieldValues).to.be.eql(SERVICE_VERSIONS);
const latencyValues = response.mainStatistics.map((item) => item.latency);
expect(latencyValues).to.be.eql([galaxyLatency, huaweiLatency]);
const throughputValues = response.mainStatistics.map((item) => item.throughput);
expect(throughputValues).to.be.eql([galaxyThroughput, huaweiThroughput]);
});
it('returns the correct data for Os version', async () => {
const response = await getMobileMainStatisticsByField({
serviceName: 'synth-android',
environment: 'production',
field: 'host.os.version',
});
const fieldValues = response.mainStatistics.map((item) => item.name);
const fieldValues = response.mainStatistics.map((item) => item.name);
expect(fieldValues).to.be.eql(OS_VERSIONS);
expect(fieldValues).to.be.eql(OS_VERSIONS);
const latencyValues = response.mainStatistics.map((item) => item.latency);
const latencyValues = response.mainStatistics.map((item) => item.latency);
expect(latencyValues).to.be.eql([galaxyLatency, huaweiLatency]);
expect(latencyValues).to.be.eql([galaxyLatency, huaweiLatency]);
const throughputValues = response.mainStatistics.map((item) => item.throughput);
expect(throughputValues).to.be.eql([galaxyThroughput, huaweiThroughput]);
});
it('returns the correct data for Devices', async () => {
const response = await getMobileMainStatisticsByField({
serviceName: 'synth-android',
environment: 'production',
field: 'device.model.identifier',
const throughputValues = response.mainStatistics.map((item) => item.throughput);
expect(throughputValues).to.be.eql([galaxyThroughput, huaweiThroughput]);
});
const fieldValues = response.mainStatistics.map((item) => item.name);
it('returns the correct data for Devices', async () => {
const response = await getMobileMainStatisticsByField({
serviceName: 'synth-android',
environment: 'production',
field: 'device.model.identifier',
});
const fieldValues = response.mainStatistics.map((item) => item.name);
expect(fieldValues).to.be.eql(['HUAWEI P2-0000', 'SM-G973F']);
expect(fieldValues).to.be.eql(['HUAWEI P2-0000', 'SM-G973F']);
const latencyValues = response.mainStatistics.map((item) => item.latency);
const latencyValues = response.mainStatistics.map((item) => item.latency);
expect(latencyValues).to.be.eql([huaweiLatency, galaxyLatency]);
expect(latencyValues).to.be.eql([huaweiLatency, galaxyLatency]);
const throughputValues = response.mainStatistics.map((item) => item.throughput);
expect(throughputValues).to.be.eql([huaweiThroughput, galaxyThroughput]);
const throughputValues = response.mainStatistics.map((item) => item.throughput);
expect(throughputValues).to.be.eql([huaweiThroughput, galaxyThroughput]);
});
});
});
});

View file

@ -0,0 +1,104 @@
/*
* 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 type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import expect from '@kbn/expect';
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
import { generateMobileData } from './generate_mobile_data';
type MostUsedCharts =
APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/most_used_charts'>;
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const synthtrace = getService('synthtrace');
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
const start = new Date('2023-01-01T00:00:00.000Z').getTime();
const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1;
async function getMobileMostUsedCharts({
environment = ENVIRONMENT_ALL.value,
kuery = '',
serviceName,
transactionType = 'mobile',
}: {
environment?: string;
kuery?: string;
serviceName: string;
transactionType?: string;
}) {
return await apmApiClient
.readUser({
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/most_used_charts',
params: {
path: { serviceName },
query: {
environment,
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
kuery,
transactionType,
},
},
})
.then(({ body }) => body);
}
describe('Most used charts', () => {
describe('when no data', () => {
it('handles empty state', async () => {
const response: MostUsedCharts = await getMobileMostUsedCharts({ serviceName: 'foo' });
expect(response.mostUsedCharts.length).to.eql(4);
expect(response.mostUsedCharts.every((chart) => chart.options.length === 0)).to.eql(true);
});
});
describe('Mobile stats', () => {
before(async () => {
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
await generateMobileData({
apmSynthtraceEsClient,
start,
end,
});
});
after(() => apmSynthtraceEsClient.clean());
describe('when data is loaded', () => {
let response: MostUsedCharts;
before(async () => {
response = await getMobileMostUsedCharts({
serviceName: 'synth-android',
environment: 'production',
});
});
it('should get the top 5 and the other option only', () => {
const deviceOptions = response.mostUsedCharts.find(
(chart) => chart.key === 'device'
)?.options;
expect(deviceOptions?.length).to.eql(6);
expect(deviceOptions?.find((option) => option.key === 'other')).to.not.be(undefined);
});
it('should get network connection type object from span events', () => {
const nctOptions = response.mostUsedCharts.find(
(chart) => chart.key === 'netConnectionType'
)?.options;
expect(nctOptions?.length).to.eql(2);
});
});
});
});
}

View file

@ -0,0 +1,133 @@
/*
* 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 type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import expect from '@kbn/expect';
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
import { generateMobileData } from './generate_mobile_data';
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const synthtrace = getService('synthtrace');
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
const start = new Date('2023-01-01T00:00:00.000Z').getTime();
const end = new Date('2023-01-01T02:00:00.000Z').getTime();
async function getSessionsChart({
environment = ENVIRONMENT_ALL.value,
kuery = '',
serviceName,
transactionType = 'mobile',
offset,
}: {
environment?: string;
kuery?: string;
serviceName: string;
transactionType?: string;
offset?: string;
}) {
return await apmApiClient.readUser({
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/transactions/charts/sessions',
params: {
path: { serviceName },
query: {
environment,
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
offset,
kuery,
transactionType,
},
},
});
}
describe('Sessions charts', () => {
describe('when no data', () => {
it('handles empty state', async () => {
const response = await getSessionsChart({ serviceName: 'foo' });
expect(response.body.currentPeriod.timeseries).to.eql([]);
expect(response.body.previousPeriod.timeseries).to.eql([]);
expect(response.status).to.be(200);
});
});
describe('with data loaded', () => {
before(async () => {
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
await generateMobileData({
apmSynthtraceEsClient,
start,
end,
});
});
after(() => apmSynthtraceEsClient.clean());
describe('when data is loaded', () => {
it('returns timeseries for sessions chart', async () => {
const response = await getSessionsChart({ serviceName: 'synth-android', offset: '1d' });
expect(response.status).to.be(200);
expect(response.body.currentPeriod.timeseries.some((item) => item.x && item.y)).to.eql(
true
);
expect(response.body.currentPeriod.timeseries[0].y).to.eql(6);
expect(response.body.previousPeriod.timeseries[0].y).to.eql(0);
});
it('returns only current period timeseries when offset is not available', async () => {
const response = await getSessionsChart({ serviceName: 'synth-android' });
expect(response.status).to.be(200);
expect(response.body.currentPeriod.timeseries.some((item) => item.x && item.y)).to.eql(
true
);
expect(response.body.currentPeriod.timeseries[0].y).to.eql(6);
expect(response.body.previousPeriod.timeseries).to.eql([]);
});
});
describe('when filters are applied', () => {
it('returns empty state for filters', async () => {
const response = await getSessionsChart({
serviceName: 'synth-android',
environment: 'production',
kuery: `app.version:"none"`,
});
expect(response.body.currentPeriod.timeseries.every((item) => item.y === 0)).to.eql(true);
expect(response.body.previousPeriod.timeseries.every((item) => item.y === 0)).to.eql(
true
);
});
it('returns the correct values filter is applied', async () => {
const response = await getSessionsChart({
serviceName: 'synth-android',
environment: 'production',
kuery: `transaction.name : "Start View - View Appearing"`,
});
expect(response.status).to.be(200);
expect(response.body.currentPeriod.timeseries.some((item) => item.x && item.y)).to.eql(
true
);
expect(response.body.currentPeriod.timeseries[0].y).to.eql(6);
expect(response.body.previousPeriod.timeseries).to.eql([]);
});
});
});
});
}

View file

@ -7,11 +7,11 @@
import expect from '@kbn/expect';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
import { meanBy, sumBy } from 'lodash';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
type MobileStats = APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/stats'>;
@ -134,10 +134,11 @@ async function generateData({
]);
}
export default function ApiTest({ getService }: FtrProviderContext) {
const apmApiClient = getService('apmApiClient');
const registry = getService('registry');
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const synthtrace = getService('synthtrace');
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
const start = new Date('2023-01-01T00:00:00.000Z').getTime();
const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1;
@ -170,7 +171,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
.then(({ body }) => body);
}
registry.when('Mobile stats when data is not loaded', { config: 'basic', archives: [] }, () => {
describe('Mobile stats', () => {
describe('when no data', () => {
it('handles empty state', async () => {
const response = await getMobileStats({ serviceName: 'foo' });
@ -182,109 +183,110 @@ export default function ApiTest({ getService }: FtrProviderContext) {
);
});
});
});
// FLAKY: https://github.com/elastic/kibana/issues/177392
registry.when.skip('Mobile stats', { config: 'basic', archives: [] }, () => {
before(async () => {
await generateData({
apmSynthtraceEsClient,
start,
end,
});
});
after(() => apmSynthtraceEsClient.clean());
describe('when data is loaded', () => {
let response: MobileStats;
describe('Mobile stats', () => {
before(async () => {
response = await getMobileStats({
serviceName: 'synth-android',
environment: 'production',
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
await generateData({
apmSynthtraceEsClient,
start,
end,
});
});
it('returns same sessions', () => {
const { value, timeseries } = response.currentPeriod.sessions;
const timeseriesTotal = sumBy(timeseries, 'y');
expect(value).to.be(timeseriesTotal);
});
after(() => apmSynthtraceEsClient.clean());
it('returns same requests', () => {
const { value, timeseries } = response.currentPeriod.requests;
const timeseriesTotal = sumBy(timeseries, 'y');
expect(value).to.be(timeseriesTotal);
});
describe('when data is loaded', () => {
let response: MobileStats;
it('returns same crashes', () => {
const { value, timeseries } = response.currentPeriod.crashRate;
const timeseriesMean = meanBy(
timeseries.filter((bucket) => bucket.y !== 0),
'y'
);
expect(value).to.be(timeseriesMean);
});
it('returns same launch times', () => {
const { value, timeseries } = response.currentPeriod.launchTimes;
const timeseriesMean = meanBy(
timeseries.filter((bucket) => bucket.y !== null),
'y'
);
expect(value).to.be(timeseriesMean);
});
});
describe('when filters are applied', () => {
it('returns empty state for filters', async () => {
const response = await getMobileStats({
serviceName: 'synth-android',
environment: 'production',
kuery: `app.version:"none"`,
before(async () => {
response = await getMobileStats({
serviceName: 'synth-android',
environment: 'production',
});
});
expect(response.currentPeriod.sessions.value).to.eql(0);
expect(response.currentPeriod.requests.value).to.eql(0);
expect(response.currentPeriod.crashRate.value).to.eql(0);
expect(response.currentPeriod.launchTimes.value).to.eql(null);
expect(response.currentPeriod.sessions.timeseries.every((item) => item.y === 0)).to.eql(
true
);
expect(response.currentPeriod.requests.timeseries.every((item) => item.y === 0)).to.eql(
true
);
expect(response.currentPeriod.crashRate.timeseries.every((item) => item.y === 0)).to.eql(
true
);
expect(
response.currentPeriod.launchTimes.timeseries.every((item) => item.y === null)
).to.eql(true);
});
it('returns the correct values when single filter is applied', async () => {
const response = await getMobileStats({
serviceName: 'synth-android',
environment: 'production',
kuery: `service.version:"2.3"`,
it('returns same sessions', () => {
const { value, timeseries } = response.currentPeriod.sessions;
const timeseriesTotal = sumBy(timeseries, 'y');
expect(value).to.be(timeseriesTotal);
});
expect(response.currentPeriod.sessions.value).to.eql(3);
expect(response.currentPeriod.requests.value).to.eql(0);
expect(response.currentPeriod.crashRate.value).to.eql(3);
expect(response.currentPeriod.launchTimes.value).to.eql(null);
it('returns same requests', () => {
const { value, timeseries } = response.currentPeriod.requests;
const timeseriesTotal = sumBy(timeseries, 'y');
expect(value).to.be(timeseriesTotal);
});
it('returns same crashes', () => {
const { value, timeseries } = response.currentPeriod.crashRate;
const timeseriesMean = meanBy(
timeseries.filter((bucket) => bucket.y !== 0),
'y'
);
expect(value).to.be(timeseriesMean);
});
it('returns same launch times', () => {
const { value, timeseries } = response.currentPeriod.launchTimes;
const timeseriesMean = meanBy(
timeseries.filter((bucket) => bucket.y !== null),
'y'
);
expect(value).to.be(timeseriesMean);
});
});
it('returns the correct values when multiple filters are applied', async () => {
const response = await getMobileStats({
serviceName: 'synth-android',
kuery: `service.version:"1.2" and service.environment: "production"`,
describe('when filters are applied', () => {
it('returns empty state for filters', async () => {
const response = await getMobileStats({
serviceName: 'synth-android',
environment: 'production',
kuery: `app.version:"none"`,
});
expect(response.currentPeriod.sessions.value).to.eql(0);
expect(response.currentPeriod.requests.value).to.eql(0);
expect(response.currentPeriod.crashRate.value).to.eql(0);
expect(response.currentPeriod.launchTimes.value).to.eql(null);
expect(response.currentPeriod.sessions.timeseries.every((item) => item.y === 0)).to.eql(
true
);
expect(response.currentPeriod.requests.timeseries.every((item) => item.y === 0)).to.eql(
true
);
expect(response.currentPeriod.crashRate.timeseries.every((item) => item.y === 0)).to.eql(
true
);
expect(
response.currentPeriod.launchTimes.timeseries.every((item) => item.y === null)
).to.eql(true);
});
it('returns the correct values when single filter is applied', async () => {
const response = await getMobileStats({
serviceName: 'synth-android',
environment: 'production',
kuery: `service.version:"2.3"`,
});
expect(response.currentPeriod.sessions.value).to.eql(3);
expect(response.currentPeriod.requests.value).to.eql(0);
expect(response.currentPeriod.crashRate.value).to.eql(3);
expect(response.currentPeriod.launchTimes.value).to.eql(null);
});
it('returns the correct values when multiple filters are applied', async () => {
const response = await getMobileStats({
serviceName: 'synth-android',
kuery: `service.version:"1.2" and service.environment: "production"`,
});
expect(response.currentPeriod.sessions.value).to.eql(3);
expect(response.currentPeriod.requests.value).to.eql(3);
expect(response.currentPeriod.crashRate.value).to.eql(1);
expect(response.currentPeriod.launchTimes.value).to.eql(100);
});
expect(response.currentPeriod.sessions.value).to.eql(3);
expect(response.currentPeriod.requests.value).to.eql(3);
expect(response.currentPeriod.crashRate.value).to.eql(1);
expect(response.currentPeriod.launchTimes.value).to.eql(100);
});
});
});

View file

@ -7,9 +7,9 @@
import expect from '@kbn/expect';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
// we generate 3 transactions per each mobile device
// timerange 15min, interval 5m, rate 1
@ -124,10 +124,11 @@ async function generateData({
]);
}
export default function ApiTest({ getService }: FtrProviderContext) {
const apmApiClient = getService('apmApiClient');
const registry = getService('registry');
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const synthtrace = getService('synthtrace');
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
const start = new Date('2023-01-01T00:00:00.000Z').getTime();
const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1;
@ -163,7 +164,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
.then(({ body }) => body);
}
registry.when('Mobile terms when data is not loaded', { config: 'basic', archives: [] }, () => {
describe('Mobile terms', () => {
describe('when no data', () => {
it('handles empty state', async () => {
const response = await getMobileTermsByField({
@ -183,66 +184,67 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(response.terms).to.eql([]);
});
});
});
// FLAKY: https://github.com/elastic/kibana/issues/177498
registry.when.skip('Mobile terms', { config: 'basic', archives: [] }, () => {
before(async () => {
await generateData({
apmSynthtraceEsClient,
start,
end,
});
});
describe('Mobile terms', () => {
before(async () => {
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
after(() => apmSynthtraceEsClient.clean());
describe('when data is loaded', () => {
it('returns mobile devices', async () => {
const response = await getMobileTermsByField({
serviceName: 'synth-android',
environment: 'production',
fieldName: 'device.model.identifier',
size: 10,
await generateData({
apmSynthtraceEsClient,
start,
end,
});
expect(response.terms).to.eql([
{ label: 'HUAWEI P2-0000', count: 3 },
{ label: 'SM-G973F', count: 3 },
]);
});
it('returns mobile versions', async () => {
const response = await getMobileTermsByField({
serviceName: 'synth-android',
environment: 'production',
fieldName: 'service.version',
size: 10,
});
expect(response.terms).to.eql([
{
label: '1.2',
count: 3,
},
{
label: '2.3',
count: 3,
},
]);
});
after(() => apmSynthtraceEsClient.clean());
it('return the most used mobile version', async () => {
const response = await getMobileTermsByField({
serviceName: 'synth-android',
environment: 'production',
fieldName: 'service.version',
size: 1,
describe('when data is loaded', () => {
it('returns mobile devices', async () => {
const response = await getMobileTermsByField({
serviceName: 'synth-android',
environment: 'production',
fieldName: 'device.model.identifier',
size: 10,
});
expect(response.terms).to.eql([
{ label: 'HUAWEI P2-0000', count: 3 },
{ label: 'SM-G973F', count: 3 },
]);
});
it('returns mobile versions', async () => {
const response = await getMobileTermsByField({
serviceName: 'synth-android',
environment: 'production',
fieldName: 'service.version',
size: 10,
});
expect(response.terms).to.eql([
{
label: '1.2',
count: 3,
},
{
label: '2.3',
count: 3,
},
]);
});
it('return the most used mobile version', async () => {
const response = await getMobileTermsByField({
serviceName: 'synth-android',
environment: 'production',
fieldName: 'service.version',
size: 1,
});
expect(response.terms).to.eql([
{
label: '1.2',
count: 3,
},
]);
});
expect(response.terms).to.eql([
{
label: '1.2',
count: 3,
},
]);
});
});
});

View file

@ -1,157 +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/mobile-services/{serviceName}/crashes/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-swift';
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/mobile-services/{serviceName}/crashes/groups/main_statistics'>['params']
>
) {
return await apmApiClient.readUser({
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/crashes/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/177651
registry.when.skip('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: 'swift' })
.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
.crash({
message: 'crash 1',
})
.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
.crash({
message: 'crash 2',
})
.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 crashes', () => {
expect(errorGroups.length).to.equal(2);
expect(errorGroups.map((error) => error.name).sort()).to.eql(['crash 1', 'crash 2']);
});
it('returns correct occurrences', () => {
const numberOfBuckets = 15;
expect(errorGroups.map((error) => error.occurrences).sort()).to.eql([
appleTransaction.failureRate * numberOfBuckets,
bananaTransaction.failureRate * numberOfBuckets,
]);
});
});
});
});
}

View file

@ -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/mobile-services/{serviceName}/crashes/distribution'>;
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
const serviceName = 'synth-swift';
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/mobile-services/{serviceName}/crashes/distribution'>['params']
>
) {
const response = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/crashes/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/177652
registry.when.skip('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
);
});
});
});
});
});
}

View file

@ -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/177654
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/177665
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));
});
});
});
}

View file

@ -1,105 +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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { generateMobileData } from './generate_mobile_data';
type MostUsedCharts =
APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/most_used_charts'>;
export default function ApiTest({ getService }: FtrProviderContext) {
const apmApiClient = getService('apmApiClient');
const registry = getService('registry');
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
const start = new Date('2023-01-01T00:00:00.000Z').getTime();
const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1;
async function getMobileMostUsedCharts({
environment = ENVIRONMENT_ALL.value,
kuery = '',
serviceName,
transactionType = 'mobile',
}: {
environment?: string;
kuery?: string;
serviceName: string;
transactionType?: string;
}) {
return await apmApiClient
.readUser({
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/most_used_charts',
params: {
path: { serviceName },
query: {
environment,
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
kuery,
transactionType,
},
},
})
.then(({ body }) => body);
}
registry.when(
'Most used charts when data is not loaded',
{ config: 'basic', archives: [] },
() => {
describe('when no data', () => {
it('handles empty state', async () => {
const response: MostUsedCharts = await getMobileMostUsedCharts({ serviceName: 'foo' });
expect(response.mostUsedCharts.length).to.eql(4);
expect(response.mostUsedCharts.every((chart) => chart.options.length === 0)).to.eql(true);
});
});
}
);
// FLAKY: https://github.com/elastic/kibana/issues/177394
registry.when.skip('Mobile stats', { config: 'basic', archives: [] }, () => {
before(async () => {
await generateMobileData({
apmSynthtraceEsClient,
start,
end,
});
});
after(() => apmSynthtraceEsClient.clean());
describe('when data is loaded', () => {
let response: MostUsedCharts;
before(async () => {
response = await getMobileMostUsedCharts({
serviceName: 'synth-android',
environment: 'production',
});
});
it('should get the top 5 and the other option only', () => {
const deviceOptions = response.mostUsedCharts.find(
(chart) => chart.key === 'device'
)?.options;
expect(deviceOptions?.length).to.eql(6);
expect(deviceOptions?.find((option) => option.key === 'other')).to.not.be(undefined);
});
it('should get network connection type object from span events', () => {
const nctOptions = response.mostUsedCharts.find(
(chart) => chart.key === 'netConnectionType'
)?.options;
expect(nctOptions?.length).to.eql(2);
});
});
});
}

View file

@ -1,128 +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 { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { generateMobileData } from './generate_mobile_data';
export default function ApiTest({ getService }: FtrProviderContext) {
const apmApiClient = getService('apmApiClient');
const registry = getService('registry');
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
const start = new Date('2023-01-01T00:00:00.000Z').getTime();
const end = new Date('2023-01-01T02:00:00.000Z').getTime();
async function getSessionsChart({
environment = ENVIRONMENT_ALL.value,
kuery = '',
serviceName,
transactionType = 'mobile',
offset,
}: {
environment?: string;
kuery?: string;
serviceName: string;
transactionType?: string;
offset?: string;
}) {
return await apmApiClient.readUser({
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/transactions/charts/sessions',
params: {
path: { serviceName },
query: {
environment,
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
offset,
kuery,
transactionType,
},
},
});
}
registry.when.skip('without data loaded', { config: 'basic', archives: [] }, () => {
describe('when no data', () => {
it('handles empty state', async () => {
const response = await getSessionsChart({ serviceName: 'foo' });
expect(response.body.currentPeriod.timeseries).to.eql([]);
expect(response.body.previousPeriod.timeseries).to.eql([]);
expect(response.status).to.be(200);
});
});
});
// FLAKY: https://github.com/elastic/kibana/issues/177393
registry.when.skip('with data loaded', { config: 'basic', archives: [] }, () => {
before(async () => {
await generateMobileData({
apmSynthtraceEsClient,
start,
end,
});
});
after(() => apmSynthtraceEsClient.clean());
describe('when data is loaded', () => {
it('returns timeseries for sessions chart', async () => {
const response = await getSessionsChart({ serviceName: 'synth-android', offset: '1d' });
expect(response.status).to.be(200);
expect(response.body.currentPeriod.timeseries.some((item) => item.x && item.y)).to.eql(
true
);
expect(response.body.currentPeriod.timeseries[0].y).to.eql(6);
expect(response.body.previousPeriod.timeseries[0].y).to.eql(0);
});
it('returns only current period timeseries when offset is not available', async () => {
const response = await getSessionsChart({ serviceName: 'synth-android' });
expect(response.status).to.be(200);
expect(response.body.currentPeriod.timeseries.some((item) => item.x && item.y)).to.eql(
true
);
expect(response.body.currentPeriod.timeseries[0].y).to.eql(6);
expect(response.body.previousPeriod.timeseries).to.eql([]);
});
});
describe('when filters are applied', () => {
it('returns empty state for filters', async () => {
const response = await getSessionsChart({
serviceName: 'synth-android',
environment: 'production',
kuery: `app.version:"none"`,
});
expect(response.body.currentPeriod.timeseries.every((item) => item.y === 0)).to.eql(true);
expect(response.body.previousPeriod.timeseries.every((item) => item.y === 0)).to.eql(true);
});
it('returns the correct values filter is applied', async () => {
const response = await getSessionsChart({
serviceName: 'synth-android',
environment: 'production',
kuery: `transaction.name : "Start View - View Appearing"`,
});
expect(response.status).to.be(200);
expect(response.body.currentPeriod.timeseries.some((item) => item.x && item.y)).to.eql(
true
);
expect(response.body.currentPeriod.timeseries[0].y).to.eql(6);
expect(response.body.previousPeriod.timeseries).to.eql([]);
});
});
});
}

View file

@ -187,6 +187,6 @@
"@kbn/alerting-types",
"@kbn/ai-assistant-common",
"@kbn/core-deprecations-common",
"@kbn/usage-collection-plugin",
"@kbn/usage-collection-plugin"
]
}