[APM] Service inventory: detailed stats fetched for all services (#134844)

* fixing service inventory detailed api call

* fixing error overview page

* fixing test

* addressing pr comments

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2022-06-24 10:29:47 -04:00 committed by GitHub
parent 17510b8863
commit b870b621fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 430 additions and 110 deletions

View file

@ -8,7 +8,7 @@
import url from 'url';
import { synthtrace } from '../../../../synthtrace';
import { checkA11y } from '../../../support/commands';
import { generateData } from './generate_data';
import { generateData, generateErrors } from './generate_data';
const start = '2021-10-10T00:00:00.000Z';
const end = '2021-10-10T00:15:00.000Z';
@ -110,3 +110,67 @@ describe('Errors page', () => {
});
});
});
describe('Check detailed statistics API with multiple errors', () => {
before(async () => {
cy.loginAsViewerUser();
await synthtrace.index(
generateErrors({
from: new Date(start).getTime(),
to: new Date(end).getTime(),
errorCount: 50,
})
);
});
after(async () => {
await synthtrace.clean();
});
it('calls detailed API with visible items only', () => {
cy.intercept(
'GET',
'/internal/apm/services/opbeans-java/errors/groups/main_statistics?*'
).as('errorsMainStatistics');
cy.intercept(
'POST',
'/internal/apm/services/opbeans-java/errors/groups/detailed_statistics?*'
).as('errorsDetailedStatistics');
cy.visit(`${javaServiceErrorsPageHref}&pageSize=10`);
cy.wait('@errorsMainStatistics');
cy.get('.euiPagination__list').children().should('have.length', 5);
cy.wait('@errorsDetailedStatistics').then((payload) => {
expect(payload.request.body.groupIds).eql(
JSON.stringify([
'0000000000000000000000000Error 0',
'0000000000000000000000000Error 1',
'0000000000000000000000000Error 2',
'0000000000000000000000000Error 3',
'0000000000000000000000000Error 4',
'0000000000000000000000000Error 5',
'0000000000000000000000000Error 6',
'0000000000000000000000000Error 7',
'0000000000000000000000000Error 8',
'0000000000000000000000000Error 9',
])
);
});
cy.get('[data-test-subj="pagination-button-1"]').click();
cy.wait('@errorsDetailedStatistics').then((payload) => {
expect(payload.request.body.groupIds).eql(
JSON.stringify([
'000000000000000000000000Error 10',
'000000000000000000000000Error 11',
'000000000000000000000000Error 12',
'000000000000000000000000Error 13',
'000000000000000000000000Error 14',
'000000000000000000000000Error 15',
'000000000000000000000000Error 16',
'000000000000000000000000Error 17',
'000000000000000000000000Error 18',
'000000000000000000000000Error 19',
])
);
});
});
});

View file

@ -39,3 +39,40 @@ export function generateData({ from, to }: { from: number; to: number }) {
.success(),
]);
}
export function generateErrors({
from,
to,
errorCount,
}: {
from: number;
to: number;
errorCount: number;
}) {
const range = timerange(from, to);
const opbeansJava = apm
.service('opbeans-java', 'production', 'java')
.instance('opbeans-java-prod-1')
.podId('opbeans-java-prod-1-pod');
return range
.interval('2m')
.rate(1)
.generator((timestamp, index) => [
opbeansJava
.transaction('GET /apple 🍎 ')
.timestamp(timestamp)
.duration(1000)
.success()
.errors(
...Array(errorCount)
.fill(0)
.map((_, idx) => {
return opbeansJava
.error(`Error ${idx}`, `exception ${idx}`)
.timestamp(timestamp);
})
),
]);
}

View file

@ -0,0 +1,38 @@
/*
* 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 { apm, timerange } from '@elastic/apm-synthtrace';
export function generateMultipleServicesData({
from,
to,
}: {
from: number;
to: number;
}) {
const range = timerange(from, to);
const services = Array(50)
.fill(0)
.map((_, idx) =>
apm
.service(`${idx}`, 'production', 'nodejs')
.instance('opbeans-node-prod-1')
);
return range
.interval('2m')
.rate(1)
.generator((timestamp, index) =>
services.map((service) =>
service
.transaction('GET /foo')
.timestamp(timestamp)
.duration(500)
.success()
)
);
}

View file

@ -9,6 +9,7 @@ import url from 'url';
import { synthtrace } from '../../../../synthtrace';
import { opbeans } from '../../../fixtures/synthtrace/opbeans';
import { checkA11y } from '../../../support/commands';
import { generateMultipleServicesData } from './generate_data';
const timeRange = {
rangeFrom: '2021-10-10T00:00:00.000Z',
@ -132,3 +133,68 @@ describe('When navigating to the service inventory', () => {
});
});
});
describe('Check detailed statistics API with multiple services', () => {
before(async () => {
cy.loginAsViewerUser();
const { rangeFrom, rangeTo } = timeRange;
await synthtrace.index(
generateMultipleServicesData({
from: new Date(rangeFrom).getTime(),
to: new Date(rangeTo).getTime(),
})
);
});
after(async () => {
await synthtrace.clean();
});
it('calls detailed API with visible items only', () => {
cy.intercept('POST', '/internal/apm/services/detailed_statistics?*').as(
'detailedStatisticsRequest'
);
cy.intercept('GET', '/internal/apm/services?*').as('mainStatisticsRequest');
cy.visit(
`${serviceInventoryHref}&pageSize=10&sortField=serviceName&sortDirection=asc`
);
cy.wait('@mainStatisticsRequest');
cy.contains('Services');
cy.get('.euiPagination__list').children().should('have.length', 5);
cy.wait('@detailedStatisticsRequest').then((payload) => {
expect(payload.request.body.serviceNames).eql(
JSON.stringify([
'0',
'1',
'10',
'11',
'12',
'13',
'14',
'15',
'16',
'17',
])
);
});
cy.get('[data-test-subj="pagination-button-1"]').click();
cy.wait('@detailedStatisticsRequest').then((payload) => {
expect(payload.request.body.serviceNames).eql(
JSON.stringify([
'18',
'19',
'2',
'20',
'21',
'22',
'23',
'24',
'25',
'26',
])
);
});
});
});

View file

@ -55,13 +55,15 @@ const Culprit = euiStyled.div`
type ErrorGroupItem =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['errorGroups'][0];
type ErrorGroupDetailedStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
interface Props {
mainStatistics: ErrorGroupItem[];
serviceName: string;
detailedStatisticsLoading: boolean;
detailedStatistics: ErrorGroupDetailedStatistics;
initialSortField: string;
initialSortDirection: 'asc' | 'desc';
comparisonEnabled?: boolean;
}
@ -71,6 +73,8 @@ function ErrorGroupList({
detailedStatisticsLoading,
detailedStatistics,
comparisonEnabled,
initialSortField,
initialSortDirection,
}: Props) {
const { query } = useApmParams('/services/{serviceName}/errors');
const { offset } = query;
@ -251,8 +255,8 @@ function ErrorGroupList({
})}
items={mainStatistics}
columns={columns}
initialSortField="occurrences"
initialSortDirection="desc"
initialSortField={initialSortField}
initialSortDirection={initialSortDirection}
sortItems={false}
/>
);

View file

@ -15,6 +15,7 @@ import {
import { i18n } from '@kbn/i18n';
import React from 'react';
import uuid from 'uuid';
import { orderBy } from 'lodash';
import { isTimeComparison } from '../../shared/time_comparison/get_comparison_options';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
@ -26,18 +27,21 @@ import { APIReturnType } from '../../../services/rest/create_call_apm_api';
import { FailedTransactionRateChart } from '../../shared/charts/failed_transaction_rate_chart';
import { ErrorDistribution } from '../error_group_details/distribution';
import { ErrorGroupList } from './error_group_list';
import { INITIAL_PAGE_SIZE } from '../../shared/managed_table';
type ErrorGroupMainStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>;
type ErrorGroupDetailedStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
const INITIAL_STATE_MAIN_STATISTICS: {
errorGroupMainStatistics: ErrorGroupMainStatistics['errorGroups'];
requestId?: string;
currentPageGroupIds: ErrorGroupMainStatistics['errorGroups'];
} = {
errorGroupMainStatistics: [],
requestId: undefined,
currentPageGroupIds: [],
};
const INITIAL_STATE_DETAILED_STATISTICS: ErrorGroupDetailedStatistics = {
@ -52,12 +56,14 @@ export function ErrorGroupOverview() {
query: {
environment,
kuery,
sortField,
sortDirection,
sortField = 'occurrences',
sortDirection = 'desc',
rangeFrom,
rangeTo,
offset,
comparisonEnabled,
page = 0,
pageSize = INITIAL_PAGE_SIZE,
},
} = useApmParams('/services/{serviceName}/errors');
@ -94,27 +100,48 @@ export function ErrorGroupOverview() {
},
}
).then((response) => {
const currentPageGroupIds = orderBy(
response.errorGroups,
sortField,
sortDirection
)
.slice(page * pageSize, (page + 1) * pageSize)
.map(({ groupId }) => groupId)
.sort();
return {
// Everytime the main statistics is refetched, updates the requestId making the comparison API to be refetched.
requestId: uuid(),
errorGroupMainStatistics: response.errorGroups,
currentPageGroupIds,
};
});
}
},
[environment, kuery, serviceName, start, end, sortField, sortDirection]
[
environment,
kuery,
serviceName,
start,
end,
sortField,
sortDirection,
page,
pageSize,
]
);
const { requestId, errorGroupMainStatistics } = errorGroupListData;
const { requestId, errorGroupMainStatistics, currentPageGroupIds } =
errorGroupListData;
const {
data: errorGroupDetailedStatistics = INITIAL_STATE_DETAILED_STATISTICS,
status: errorGroupDetailedStatisticsStatus,
} = useFetcher(
(callApmApi) => {
if (requestId && errorGroupMainStatistics.length && start && end) {
if (requestId && currentPageGroupIds.length && start && end) {
return callApmApi(
'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
{
params: {
path: { serviceName },
@ -124,14 +151,14 @@ export function ErrorGroupOverview() {
start,
end,
numBuckets: 20,
groupIds: JSON.stringify(
errorGroupMainStatistics.map(({ groupId }) => groupId).sort()
),
offset:
comparisonEnabled && isTimeComparison(offset)
? offset
: undefined,
},
body: {
groupIds: JSON.stringify(currentPageGroupIds),
},
},
}
);
@ -192,6 +219,8 @@ export function ErrorGroupOverview() {
}
detailedStatistics={errorGroupDetailedStatistics}
comparisonEnabled={comparisonEnabled}
initialSortField={sortField}
initialSortDirection={sortDirection}
/>
</EuiPanel>
</EuiFlexItem>

View file

@ -24,6 +24,7 @@ import { useProgressiveFetcher } from '../../../hooks/use_progressive_fetcher';
import { joinByKey } from '../../../../common/utils/join_by_key';
import { ServiceInventoryFieldName } from '../../../../common/service_inventory';
import { orderServiceItems } from './service_list/order_service_items';
import { INITIAL_PAGE_SIZE } from '../../shared/managed_table';
const initialData = {
requestId: '',
@ -32,7 +33,7 @@ const initialData = {
hasLegacyData: false,
};
function useServicesFetcher() {
function useServicesMainStatisticsFetcher() {
const {
query: {
rangeFrom,
@ -40,8 +41,10 @@ function useServicesFetcher() {
environment,
kuery,
serviceGroup,
offset,
comparisonEnabled,
page = 0,
pageSize = INITIAL_PAGE_SIZE,
sortDirection,
sortField,
},
} = useApmParams('/services');
@ -85,37 +88,94 @@ function useServicesFetcher() {
});
}
},
[environment, kuery, start, end, serviceGroup]
// eslint-disable-next-line react-hooks/exhaustive-deps
[
environment,
kuery,
start,
end,
serviceGroup,
// not used, but needed to update the requestId to call the details statistics API when table is options are updated
page,
pageSize,
sortField,
sortDirection,
]
);
return {
sortedAndFilteredServicesFetch,
mainStatisticsFetch,
};
}
function useServicesDetailedStatisticsFetcher({
mainStatisticsFetch,
initialSortField,
initialSortDirection,
tiebreakerField,
}: {
mainStatisticsFetch: ReturnType<
typeof useServicesMainStatisticsFetcher
>['mainStatisticsFetch'];
initialSortField: ServiceInventoryFieldName;
initialSortDirection: 'asc' | 'desc';
tiebreakerField: ServiceInventoryFieldName;
}) {
const {
query: {
rangeFrom,
rangeTo,
environment,
kuery,
offset,
comparisonEnabled,
page = 0,
pageSize = INITIAL_PAGE_SIZE,
sortDirection = initialSortDirection,
sortField = initialSortField,
},
} = useApmParams('/services');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const { data: mainStatisticsData = initialData } = mainStatisticsFetch;
const currentPageItems = orderServiceItems({
items: mainStatisticsData.items,
primarySortField: sortField as ServiceInventoryFieldName,
sortDirection,
tiebreakerField,
}).slice(page * pageSize, (page + 1) * pageSize);
const comparisonFetch = useProgressiveFetcher(
(callApmApi) => {
if (
start &&
end &&
mainStatisticsData.items.length &&
currentPageItems.length &&
mainStatisticsFetch.status === FETCH_STATUS.SUCCESS
) {
return callApmApi('GET /internal/apm/services/detailed_statistics', {
return callApmApi('POST /internal/apm/services/detailed_statistics', {
params: {
query: {
environment,
kuery,
start,
end,
serviceNames: JSON.stringify(
mainStatisticsData.items
.map(({ serviceName }) => serviceName)
// Service name is sorted to guarantee the same order every time this API is called so the result can be cached.
.sort()
),
offset:
comparisonEnabled && isTimeComparison(offset)
? offset
: undefined,
},
body: {
serviceNames: JSON.stringify(
currentPageItems
.map(({ serviceName }) => serviceName)
// Service name is sorted to guarantee the same order every time this API is called so the result can be cached.
.sort()
),
},
},
});
}
@ -126,19 +186,43 @@ function useServicesFetcher() {
{ preservePreviousData: false }
);
return {
sortedAndFilteredServicesFetch,
mainStatisticsFetch,
comparisonFetch,
};
return { comparisonFetch };
}
export function ServiceInventory() {
const {
sortedAndFilteredServicesFetch,
const { sortedAndFilteredServicesFetch, mainStatisticsFetch } =
useServicesMainStatisticsFetcher();
const mainStatisticsItems = mainStatisticsFetch.data?.items ?? [];
const preloadedServices = sortedAndFilteredServicesFetch.data?.services || [];
const displayHealthStatus = [
...mainStatisticsItems,
...preloadedServices,
].some((item) => 'healthStatus' in item);
const useOptimizedSorting =
useKibana().services.uiSettings?.get<boolean>(
apmServiceInventoryOptimizedSorting
) || false;
const tiebreakerField = useOptimizedSorting
? ServiceInventoryFieldName.ServiceName
: ServiceInventoryFieldName.Throughput;
const initialSortField = displayHealthStatus
? ServiceInventoryFieldName.HealthStatus
: tiebreakerField;
const initialSortDirection =
initialSortField === ServiceInventoryFieldName.ServiceName ? 'asc' : 'desc';
const { comparisonFetch } = useServicesDetailedStatisticsFetcher({
mainStatisticsFetch,
comparisonFetch,
} = useServicesFetcher();
initialSortField,
initialSortDirection,
tiebreakerField,
});
const { anomalyDetectionSetupState } = useAnomalyDetectionJobsContext();
@ -151,11 +235,6 @@ export function ServiceInventory() {
!userHasDismissedCallout &&
shouldDisplayMlCallout(anomalyDetectionSetupState);
const useOptimizedSorting =
useKibana().services.uiSettings?.get<boolean>(
apmServiceInventoryOptimizedSorting
) || false;
let isLoading: boolean;
if (useOptimizedSorting) {
@ -183,25 +262,6 @@ export function ServiceInventory() {
/>
);
const mainStatisticsItems = mainStatisticsFetch.data?.items ?? [];
const preloadedServices = sortedAndFilteredServicesFetch.data?.services || [];
const displayHealthStatus = [
...mainStatisticsItems,
...preloadedServices,
].some((item) => 'healthStatus' in item);
const tiebreakerField = useOptimizedSorting
? ServiceInventoryFieldName.ServiceName
: ServiceInventoryFieldName.Throughput;
const initialSortField = displayHealthStatus
? ServiceInventoryFieldName.HealthStatus
: tiebreakerField;
const initialSortDirection =
initialSortField === ServiceInventoryFieldName.ServiceName ? 'asc' : 'desc';
const items = joinByKey(
[
// only use preloaded services if tiebreaker field is service.name,

View file

@ -50,7 +50,7 @@ import {
} from '../../../../../common/service_inventory';
type ServicesDetailedStatisticsAPIResponse =
APIReturnType<'GET /internal/apm/services/detailed_statistics'>;
APIReturnType<'POST /internal/apm/services/detailed_statistics'>;
function formatString(value?: string | null) {
return value || NOT_AVAILABLE_LABEL;
@ -271,7 +271,16 @@ export function ServiceList({
transactionType !== TRANSACTION_PAGE_LOAD
);
const { query } = useApmParams('/services');
const {
// removes pagination and sort instructions from the query so it won't be passed down to next route
query: {
page,
pageSize,
sortDirection: direction,
sortField: field,
...query
},
} = useApmParams('/services');
const { kuery } = query;
const { fallbackToTransactions } = useFallbackToTransactionsFetcher({

View file

@ -32,7 +32,7 @@ const ErrorLink = euiStyled(ErrorOverviewLink)`
type ErrorGroupMainStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>;
type ErrorGroupDetailedStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
export function getColumns({
serviceName,

View file

@ -30,7 +30,7 @@ interface Props {
type ErrorGroupMainStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>;
type ErrorGroupDetailedStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
type SortDirection = 'asc' | 'desc';
type SortField = 'name' | 'lastSeen' | 'occurrences';
@ -137,7 +137,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
(callApmApi) => {
if (requestId && items.length && start && end) {
return callApmApi(
'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
{
params: {
path: { serviceName },
@ -147,14 +147,16 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
start,
end,
numBuckets: 20,
groupIds: JSON.stringify(
items.map(({ groupId: groupId }) => groupId).sort()
),
offset:
comparisonEnabled && isTimeComparison(offset)
? offset
: undefined,
},
body: {
groupIds: JSON.stringify(
items.map(({ groupId: groupId }) => groupId).sort()
),
},
},
}
);

View file

@ -169,6 +169,10 @@ export const home = {
t.partial({
refreshPaused: t.union([t.literal('true'), t.literal('false')]),
refreshInterval: t.string,
page: toNumberRt,
pageSize: toNumberRt,
sortField: t.string,
sortDirection: t.union([t.literal('asc'), t.literal('desc')]),
}),
offsetRt,
]),

View file

@ -48,7 +48,7 @@ interface Props<T> {
}
const PAGE_SIZE_OPTIONS = [10, 25, 50];
const INITIAL_PAGE_SIZE = 25;
export const INITIAL_PAGE_SIZE = 25;
function defaultSortFn<T extends any>(
items: T[],

View file

@ -70,7 +70,7 @@ const errorsMainStatisticsRoute = createApmServerRoute({
const errorsDetailedStatisticsRoute = createApmServerRoute({
endpoint:
'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
params: t.type({
path: t.type({
serviceName: t.string,
@ -82,9 +82,9 @@ const errorsDetailedStatisticsRoute = createApmServerRoute({
offsetRt,
t.type({
numBuckets: toNumberRt,
groupIds: jsonRt.pipe(t.array(t.string)),
}),
]),
body: t.type({ groupIds: jsonRt.pipe(t.array(t.string)) }),
}),
options: { tags: ['access:apm'] },
handler: async (
@ -107,7 +107,8 @@ const errorsDetailedStatisticsRoute = createApmServerRoute({
const {
path: { serviceName },
query: { environment, kuery, numBuckets, groupIds, start, end, offset },
query: { environment, kuery, numBuckets, start, end, offset },
body: { groupIds },
} = params;
return getErrorGroupPeriods({

View file

@ -152,19 +152,16 @@ const servicesRoute = createApmServerRoute({
});
const servicesDetailedStatisticsRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/services/detailed_statistics',
endpoint: 'POST /internal/apm/services/detailed_statistics',
params: t.type({
query: t.intersection([
// t.intersection seemingly only supports 5 arguments so let's wrap them in another intersection
t.intersection([
environmentRt,
kueryRt,
rangeRt,
offsetRt,
probabilityRt,
]),
t.type({ serviceNames: jsonRt.pipe(t.array(t.string)) }),
environmentRt,
kueryRt,
rangeRt,
offsetRt,
probabilityRt,
]),
body: t.type({ serviceNames: jsonRt.pipe(t.array(t.string)) }),
}),
options: { tags: ['access:apm'] },
handler: async (
@ -207,15 +204,10 @@ const servicesDetailedStatisticsRoute = createApmServerRoute({
plugins: { security },
} = resources;
const {
environment,
kuery,
offset,
serviceNames,
start,
end,
probability,
} = params.query;
const { environment, kuery, offset, start, end, probability } =
params.query;
const { serviceNames } = params.body;
const [setup, randomSampler] = await Promise.all([
setupRequest(resources),

View file

@ -19,7 +19,7 @@ import { config, generateData } from './generate_data';
import { getErrorGroupIds } from './get_error_group_ids';
type ErrorGroupsDetailedStatistics =
APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>;
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
@ -32,22 +32,22 @@ export default function ApiTest({ getService }: FtrProviderContext) {
async function callApi(
overrides?: RecursivePartial<
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>['params']
APIClientRequestParamsOf<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>['params']
>
) {
return await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics`,
endpoint: `POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics`,
params: {
path: { serviceName, ...overrides?.path },
query: {
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
numBuckets: 20,
groupIds: JSON.stringify(['foo']),
environment: 'ENVIRONMENT_ALL',
kuery: '',
...overrides?.query,
},
body: { groupIds: JSON.stringify(['foo']), ...overrides?.body },
},
});
}
@ -82,7 +82,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
before(async () => {
errorIds = await getErrorGroupIds({ serviceName, start, end, apmApiClient });
const response = await callApi({
query: {
body: {
groupIds: JSON.stringify(errorIds),
},
});
@ -116,7 +116,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
let errorGroupsDetailedStatistics: ErrorGroupsDetailedStatistics;
before(async () => {
const response = await callApi({
query: {
body: {
groupIds: JSON.stringify(['foo']),
},
});
@ -138,11 +138,13 @@ export default function ApiTest({ getService }: FtrProviderContext) {
errorIds = await getErrorGroupIds({ serviceName, start, end, apmApiClient });
const response = await callApi({
query: {
groupIds: JSON.stringify(errorIds),
start: moment(end).subtract(7, 'minutes').toISOString(),
end: new Date(end).toISOString(),
offset: '7m',
},
body: {
groupIds: JSON.stringify(errorIds),
},
});
errorGroupsDetailedStatistics = response.body;
});

View file

@ -13,7 +13,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context';
import { ApmApiError } from '../../common/apm_api_supertest';
type ServicesDetailedStatisticsReturn =
APIReturnType<'GET /internal/apm/services/detailed_statistics'>;
APIReturnType<'POST /internal/apm/services/detailed_statistics'>;
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
@ -31,17 +31,19 @@ export default function ApiTest({ getService }: FtrProviderContext) {
() => {
it('handles the empty state', async () => {
const response = await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/detailed_statistics`,
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start,
end,
serviceNames: JSON.stringify(serviceNames),
environment: 'ENVIRONMENT_ALL',
kuery: '',
offset: '1d',
probability: 1,
},
body: {
serviceNames: JSON.stringify(serviceNames),
},
},
});
@ -59,16 +61,18 @@ export default function ApiTest({ getService }: FtrProviderContext) {
let servicesDetailedStatistics: ServicesDetailedStatisticsReturn;
before(async () => {
const response = await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/detailed_statistics`,
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start,
end,
serviceNames: JSON.stringify(serviceNames),
environment: 'ENVIRONMENT_ALL',
kuery: '',
probability: 1,
},
body: {
serviceNames: JSON.stringify(serviceNames),
},
},
});
expect(response.status).to.be(200);
@ -112,16 +116,18 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('returns empty when empty service names is passed', async () => {
try {
await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/detailed_statistics`,
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start,
end,
serviceNames: JSON.stringify([]),
environment: 'ENVIRONMENT_ALL',
kuery: '',
probability: 1,
},
body: {
serviceNames: JSON.stringify([]),
},
},
});
expect().fail('Expected API call to throw an error');
@ -135,16 +141,18 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('filters by environment', async () => {
const response = await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/detailed_statistics`,
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start,
end,
serviceNames: JSON.stringify(serviceNames),
environment: 'production',
kuery: '',
probability: 1,
},
body: {
serviceNames: JSON.stringify(serviceNames),
},
},
});
expect(response.status).to.be(200);
@ -153,16 +161,18 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('filters by kuery', async () => {
const response = await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/detailed_statistics`,
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start,
end,
serviceNames: JSON.stringify(serviceNames),
environment: 'ENVIRONMENT_ALL',
kuery: 'transaction.type : "invalid_transaction_type"',
probability: 1,
},
body: {
serviceNames: JSON.stringify(serviceNames),
},
},
});
expect(response.status).to.be(200);
@ -178,17 +188,19 @@ export default function ApiTest({ getService }: FtrProviderContext) {
let servicesDetailedStatistics: ServicesDetailedStatisticsReturn;
before(async () => {
const response = await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/detailed_statistics`,
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start: moment(end).subtract(15, 'minutes').toISOString(),
end,
serviceNames: JSON.stringify(serviceNames),
offset: '15m',
environment: 'ENVIRONMENT_ALL',
kuery: '',
probability: 1,
},
body: {
serviceNames: JSON.stringify(serviceNames),
},
},
});