diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 796f2992236f9..d71d5f2cb480d 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -5,9 +5,12 @@ */ import { EuiTitle } from '@elastic/eui'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; -import { scaleUtc } from 'd3-scale'; import d3 from 'd3'; +import { scaleUtc } from 'd3-scale'; +import mean from 'lodash.mean'; import React from 'react'; import { asRelativeDateTimeRange } from '../../../../utils/formatters'; import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs'; @@ -17,7 +20,7 @@ import { EmptyMessage } from '../../../shared/EmptyMessage'; interface IBucket { key: number; - count: number; + count: number | undefined; } // TODO: cleanup duplication of this in distribution/get_distribution.ts (ErrorDistributionAPIResponse) and transactions/distribution/index.ts (TransactionDistributionAPIResponse) @@ -30,7 +33,7 @@ interface IDistribution { interface FormattedBucket { x0: number; x: number; - y: number; + y: number | undefined; } export function getFormattedBuckets( @@ -64,7 +67,7 @@ export function ErrorDistribution({ distribution, title }: Props) { distribution.bucketSize ); - if (!buckets || distribution.noHits) { + if (!buckets) { return ( bucket.y)) || 0; const xMin = d3.min(buckets, (d) => d.x0); const xMax = d3.max(buckets, (d) => d.x); const tickFormat = scaleUtc().domain([xMin, xMax]).tickFormat(); @@ -84,6 +88,7 @@ export function ErrorDistribution({ distribution, title }: Props) { {title} bucket.x} xType="time-utc" @@ -105,6 +110,17 @@ export function ErrorDistribution({ distribution, title }: Props) { values: { occCount: value }, }) } + legends={[ + { + color: theme.euiColorVis1, + // 0a abbreviates large whole numbers with metric prefixes like: 1000 = 1k, 32000 = 32k, 1000000 = 1m + legendValue: numeral(averageValue).format('0a'), + title: i18n.translate('xpack.apm.errorGroupDetails.avgLabel', { + defaultMessage: 'Avg.', + }), + legendClickDisabled: true, + }, + ]} /> ); diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index 0ad8a52b740df..b765dc42ede64 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -25,6 +25,9 @@ import { ErrorDistribution } from './Distribution'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; +import { ErrorRateChart } from '../../shared/charts/ErrorRateChart'; +import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; const Titles = styled.div` margin-bottom: ${px(units.plus)}; @@ -60,49 +63,43 @@ export function ErrorGroupDetails() { const { urlParams, uiFilters } = useUrlParams(); const { serviceName, start, end, errorGroupId } = urlParams; - const { data: errorGroupData } = useFetcher( - (callApmApi) => { - if (serviceName && start && end && errorGroupId) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/{groupId}', - params: { - path: { - serviceName, - groupId: errorGroupId, - }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters), - }, + const { data: errorGroupData } = useFetcher(() => { + if (serviceName && start && end && errorGroupId) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/{groupId}', + params: { + path: { + serviceName, + groupId: errorGroupId, }, - }); - } - }, - [serviceName, start, end, errorGroupId, uiFilters] - ); - - const { data: errorDistributionData } = useFetcher( - (callApmApi) => { - if (serviceName && start && end && errorGroupId) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/distribution', - params: { - path: { - serviceName, - }, - query: { - start, - end, - groupId: errorGroupId, - uiFilters: JSON.stringify(uiFilters), - }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), }, - }); - } - }, - [serviceName, start, end, errorGroupId, uiFilters] - ); + }, + }); + } + }, [serviceName, start, end, errorGroupId, uiFilters]); + + const { data: errorDistributionData } = useFetcher(() => { + if (serviceName && start && end && errorGroupId) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/distribution', + params: { + path: { + serviceName, + }, + query: { + start, + end, + groupId: errorGroupId, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + }, [serviceName, start, end, errorGroupId, uiFilters]); useTrackPageview({ app: 'apm', path: 'error_group_details' }); useTrackPageview({ app: 'apm', path: 'error_group_details', delay: 15000 }); @@ -184,16 +181,24 @@ export function ErrorGroupDetails() { )} - - + + + + + + + + + + {showDetails && ( diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index ff031c5a86d11..73474208e26c0 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -13,64 +13,61 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; -import { ErrorGroupList } from './List'; -import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; import { PROJECTION } from '../../../../common/projections/typings'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; +import { ErrorRateChart } from '../../shared/charts/ErrorRateChart'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; +import { ErrorGroupList } from './List'; +import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; const ErrorGroupOverview: React.FC = () => { const { urlParams, uiFilters } = useUrlParams(); const { serviceName, start, end, sortField, sortDirection } = urlParams; - const { data: errorDistributionData } = useFetcher( - (callApmApi) => { - if (serviceName && start && end) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/distribution', - params: { - path: { - serviceName, - }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters), - }, + const { data: errorDistributionData } = useFetcher(() => { + if (serviceName && start && end) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/distribution', + params: { + path: { + serviceName, }, - }); - } - }, - [serviceName, start, end, uiFilters] - ); + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + }, [serviceName, start, end, uiFilters]); - const { data: errorGroupListData } = useFetcher( - (callApmApi) => { - const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; + const { data: errorGroupListData } = useFetcher(() => { + const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; - if (serviceName && start && end) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors', - params: { - path: { - serviceName, - }, - query: { - start, - end, - sortField, - sortDirection: normalizedSortDirection, - uiFilters: JSON.stringify(uiFilters), - }, + if (serviceName && start && end) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors', + params: { + path: { + serviceName, }, - }); - } - }, - [serviceName, start, end, sortField, sortDirection, uiFilters] - ); + query: { + start, + end, + sortField, + sortDirection: normalizedSortDirection, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + }, [serviceName, start, end, sortField, sortDirection, uiFilters]); useTrackPageview({ app: 'apm', @@ -102,20 +99,27 @@ const ErrorGroupOverview: React.FC = () => { - - - - - - + + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js index 7d37d2d67269b..2c4cc185dac7e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js @@ -108,7 +108,9 @@ export default function Legends({ return ( clickLegend(i)} + onClick={ + serie.legendClickDisabled ? undefined : () => clickLegend(i) + } disabled={seriesEnabledState[i]} text={text} color={serie.color} diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js index e1ffec3a8d97f..7e74961e57ea1 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js @@ -144,7 +144,7 @@ export class InnerCustomPlot extends PureComponent { const hasValidCoordinates = flatten(series.map((s) => s.data)).some((p) => isValidCoordinateValue(p.y) ); - const noHits = !hasValidCoordinates; + const noHits = this.props.noHits || !hasValidCoordinates; const plotValues = this.getPlotValues({ visibleSeries, @@ -234,6 +234,7 @@ InnerCustomPlot.propTypes = { firstSeen: PropTypes.number, }) ), + noHits: PropTypes.bool, }; InnerCustomPlot.defaultProps = { @@ -241,6 +242,8 @@ InnerCustomPlot.defaultProps = { tickFormatX: undefined, tickFormatY: (y) => y, truncateLegends: false, + xAxisTickSizeOuter: 0, + noHits: false, }; export default makeWidthFlexible(InnerCustomPlot); diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx new file mode 100644 index 0000000000000..7aafa9e1fdcec --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiTitle } from '@elastic/eui'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import mean from 'lodash.mean'; +import React, { useCallback } from 'react'; +import { useChartsSync } from '../../../../hooks/useChartsSync'; +import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; +import { unit } from '../../../../style/variables'; +import { asPercent } from '../../../../utils/formatters'; +// @ts-ignore +import CustomPlot from '../CustomPlot'; + +const tickFormatY = (y?: number) => { + return asPercent(y || 0, 1); +}; + +export const ErrorRateChart = () => { + const { urlParams, uiFilters } = useUrlParams(); + const syncedChartsProps = useChartsSync(); + + const { serviceName, start, end, errorGroupId } = urlParams; + const { data: errorRateData } = useFetcher(() => { + if (serviceName && start && end) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/rate', + params: { + path: { + serviceName, + }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + groupId: errorGroupId, + }, + }, + }); + } + }, [serviceName, start, end, uiFilters, errorGroupId]); + + const combinedOnHover = useCallback( + (hoverX: number) => { + return syncedChartsProps.onHover(hoverX); + }, + [syncedChartsProps] + ); + + const errorRates = errorRateData?.errorRates || []; + + return ( + <> + + + {i18n.translate('xpack.apm.errorRateChart.title', { + defaultMessage: 'Error Rate', + })} + + + rate.y))), + legendClickDisabled: true, + title: i18n.translate('xpack.apm.errorRateChart.avgLabel', { + defaultMessage: 'Avg.', + }), + type: 'linemark', + hideTooltipValue: true, + }, + { + data: errorRates, + type: 'line', + color: theme.euiColorVis7, + hideLegend: true, + title: i18n.translate('xpack.apm.errorRateChart.rateLabel', { + defaultMessage: 'Rate', + }), + }, + ]} + onHover={combinedOnHover} + tickFormatY={tickFormatY} + formatTooltipValue={({ y }: { y?: number }) => + Number.isFinite(y) ? tickFormatY(y) : 'N/A' + } + height={unit * 10} + /> + + ); +}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap index 1f935af7c8999..a31b9735628ab 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap @@ -114,7 +114,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 0 ms @@ -149,7 +149,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 500 ms @@ -184,7 +184,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 1,000 ms @@ -219,7 +219,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 1,500 ms @@ -254,7 +254,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 2,000 ms @@ -289,7 +289,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 2,500 ms @@ -324,7 +324,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 3,000 ms diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js index 4eca1a37c51bc..002ff19d0d1df 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js @@ -26,6 +26,10 @@ import Tooltip from '../Tooltip'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { tint } from 'polished'; import { getTimeTicksTZ, getDomainTZ } from '../helper/timezone'; +import Legends from '../CustomPlot/Legends'; +import StatusText from '../CustomPlot/StatusText'; +import { i18n } from '@kbn/i18n'; +import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; const XY_HEIGHT = unit * 10; const XY_MARGIN = { @@ -99,6 +103,7 @@ export class HistogramInner extends PureComponent { tooltipHeader, verticalLineHover, width: XY_WIDTH, + legends, } = this.props; const { hoveredBucket } = this.state; if (isEmpty(buckets) || XY_WIDTH === 0) { @@ -139,102 +144,140 @@ export class HistogramInner extends PureComponent { const showVerticalLineHover = verticalLineHover(hoveredBucket); const showBackgroundHover = backgroundHover(hoveredBucket); + const hasValidCoordinates = buckets.some((bucket) => + isValidCoordinateValue(bucket.y) + ); + const noHits = this.props.noHits || !hasValidCoordinates; + + const xyPlotProps = { + dontCheckIfEmpty: true, + xType: this.props.xType, + width: XY_WIDTH, + height: XY_HEIGHT, + margin: XY_MARGIN, + xDomain: xDomain, + yDomain: yDomain, + }; + + const xAxisProps = { + style: { strokeWidth: '1px' }, + marginRight: 10, + tickSize: 0, + tickTotal: X_TICK_TOTAL, + tickFormat: formatX, + tickValues: xTickValues, + }; + + const emptyStateChart = ( + + + + + ); + return (
- - - - - - {showBackgroundHover && ( - - )} - - {shouldShowTooltip && ( - - )} - - {selectedBucket && ( - - )} - - - - {showVerticalLineHover && ( - - )} - - { - return { - ...bucket, - xCenter: (bucket.x0 + bucket.x) / 2, - }; - })} - onClick={this.onClick} - onHover={this.onHover} - onBlur={this.onBlur} - x={(d) => x(d.xCenter)} - y={() => 1} - /> - + {noHits ? ( + <>{emptyStateChart} + ) : ( + <> + + + + + + {showBackgroundHover && ( + + )} + + {shouldShowTooltip && ( + + )} + + {selectedBucket && ( + + )} + + + + {showVerticalLineHover && hoveredBucket?.x && ( + + )} + + { + return { + ...bucket, + xCenter: (bucket.x0 + bucket.x) / 2, + }; + })} + onClick={this.onClick} + onHover={this.onHover} + onBlur={this.onBlur} + x={(d) => x(d.xCenter)} + y={() => 1} + /> + + + {legends && ( + {}} + truncateLegends={false} + noHits={noHits} + /> + )} + + )}
); @@ -255,6 +298,8 @@ HistogramInner.propTypes = { verticalLineHover: PropTypes.func, width: PropTypes.number.isRequired, xType: PropTypes.string, + legends: PropTypes.array, + noHits: PropTypes.bool, }; HistogramInner.defaultProps = { @@ -265,6 +310,7 @@ HistogramInner.defaultProps = { tooltipHeader: () => null, verticalLineHover: () => null, xType: 'linear', + noHits: false, }; export default makeWidthFlexible(HistogramInner); diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts new file mode 100644 index 0000000000000..d558e3942a42b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + ERROR_GROUP_ID, + PROCESSOR_EVENT, + SERVICE_NAME, +} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { getMetricsDateHistogramParams } from '../helpers/metrics'; +import { rangeFilter } from '../helpers/range_filter'; +import { + Setup, + SetupTimeRange, + SetupUIFilters, +} from '../helpers/setup_request'; + +export async function getErrorRate({ + serviceName, + groupId, + setup, +}: { + serviceName: string; + groupId?: string; + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const { start, end, uiFiltersES, client, indices } = setup; + + const filter = [ + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...uiFiltersES, + ]; + + const aggs = { + response_times: { + date_histogram: getMetricsDateHistogramParams(start, end), + }, + }; + + const getTransactionBucketAggregation = async () => { + const resp = await client.search({ + index: indices['apm_oss.transactionIndices'], + body: { + size: 0, + query: { + bool: { + filter: [ + ...filter, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + ], + }, + }, + aggs, + }, + }); + return { + totalHits: resp.hits.total.value, + responseTimeBuckets: resp.aggregations?.response_times.buckets, + }; + }; + const getErrorBucketAggregation = async () => { + const groupIdFilter = groupId + ? [{ term: { [ERROR_GROUP_ID]: groupId } }] + : []; + const resp = await client.search({ + index: indices['apm_oss.errorIndices'], + body: { + size: 0, + query: { + bool: { + filter: [ + ...filter, + ...groupIdFilter, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, + ], + }, + }, + aggs, + }, + }); + return resp.aggregations?.response_times.buckets; + }; + + const [transactions, errorResponseTimeBuckets] = await Promise.all([ + getTransactionBucketAggregation(), + getErrorBucketAggregation(), + ]); + + const transactionCountByTimestamp: Record = {}; + if (transactions?.responseTimeBuckets) { + transactions.responseTimeBuckets.forEach((bucket) => { + transactionCountByTimestamp[bucket.key] = bucket.doc_count; + }); + } + + const errorRates = errorResponseTimeBuckets?.map((bucket) => { + const { key, doc_count: errorCount } = bucket; + const relativeRate = errorCount / transactionCountByTimestamp[key]; + return { x: key, y: relativeRate }; + }); + + return { + noHits: transactions?.totalHits === 0, + errorRates, + }; +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 774f1f27435a2..bdfb49fa30828 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -13,6 +13,7 @@ import { errorDistributionRoute, errorGroupsRoute, errorsRoute, + errorRateRoute, } from './errors'; import { serviceAgentNameRoute, @@ -81,6 +82,7 @@ const createApmApi = () => { .add(errorDistributionRoute) .add(errorGroupsRoute) .add(errorsRoute) + .add(errorRateRoute) // Services .add(serviceAgentNameRoute) diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index 1615550027d3c..97314a9a61661 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -11,6 +11,7 @@ import { getErrorGroup } from '../lib/errors/get_error_group'; import { getErrorGroups } from '../lib/errors/get_error_groups'; import { setupRequest } from '../lib/helpers/setup_request'; import { uiFiltersRt, rangeRt } from './default_api_types'; +import { getErrorRate } from '../lib/errors/get_error_rate'; export const errorsRoute = createRoute(() => ({ path: '/api/apm/services/{serviceName}/errors', @@ -80,3 +81,26 @@ export const errorDistributionRoute = createRoute(() => ({ return getErrorDistribution({ serviceName, groupId, setup }); }, })); + +export const errorRateRoute = createRoute(() => ({ + path: '/api/apm/services/{serviceName}/errors/rate', + params: { + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + t.partial({ + groupId: t.string, + }), + uiFiltersRt, + rangeRt, + ]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { params } = context; + const { serviceName } = params.path; + const { groupId } = params.query; + return getErrorRate({ serviceName, groupId, setup }); + }, +}));