feat: added top errors tab in domain details

This commit is contained in:
sawhil 2025-04-25 04:24:45 +05:30 committed by Sahil Khan
parent d5e2841083
commit b86e65d2ca
5 changed files with 383 additions and 0 deletions

View File

@ -55,6 +55,7 @@ export const REACT_QUERY_KEY = {
// API Monitoring Query Keys
GET_DOMAINS_LIST: 'GET_DOMAINS_LIST',
GET_ENDPOINTS_LIST_BY_DOMAIN: 'GET_ENDPOINTS_LIST_BY_DOMAIN',
GET_TOP_ERRORS_BY_DOMAIN: 'GET_TOP_ERRORS_BY_DOMAIN',
GET_NESTED_ENDPOINTS_LIST: 'GET_NESTED_ENDPOINTS_LIST',
GET_ENDPOINT_METRICS_DATA: 'GET_ENDPOINT_METRICS_DATA',
GET_ENDPOINT_STATUS_CODE_DATA: 'GET_ENDPOINT_STATUS_CODE_DATA',

View File

@ -21,6 +21,7 @@ import AllEndPoints from './AllEndPoints';
import DomainMetrics from './components/DomainMetrics';
import { VIEW_TYPES, VIEWS } from './constants';
import EndPointDetailsWrapper from './EndPointDetailsWrapper';
import TopErrors from './TopErrors';
const TimeRangeOffset = 1000000000;
@ -181,6 +182,14 @@ function DomainDetails({
>
<div className="view-title">Endpoint Details</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.TOP_ERRORS ? 'tab selected_view' : 'tab'
}
value={VIEW_TYPES.TOP_ERRORS}
>
<div className="view-title">Top Errors</div>
</Radio.Button>
</Radio.Group>
</div>
{selectedView === VIEW_TYPES.ALL_ENDPOINTS && (
@ -203,6 +212,13 @@ function DomainDetails({
timeRange={modalTimeRange}
/>
)}
{selectedView === VIEW_TYPES.TOP_ERRORS && (
<TopErrors
domainName={domainData.domainName}
timeRange={modalTimeRange}
/>
)}
</>
)}
</Drawer>

View File

@ -0,0 +1,120 @@
import { LoadingOutlined } from '@ant-design/icons';
import { Spin, Table, Typography } from 'antd';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
formatTopErrorsDataForTable,
getTopErrorsColumnsConfig,
getTopErrorsQueryPayload,
TopErrorsResponseRow,
} from 'container/ApiMonitoring/utils';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useMemo } from 'react';
import { useQueries } from 'react-query';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import ErrorState from './components/ErrorState';
function TopErrors({
domainName,
timeRange,
}: {
domainName: string;
timeRange: {
startTime: number;
endTime: number;
};
}): JSX.Element {
const { startTime: minTime, endTime: maxTime } = timeRange;
const queryPayloads = useMemo(
() => getTopErrorsQueryPayload(domainName, minTime, maxTime),
[domainName, minTime, maxTime],
);
// Since only one query here
const topErrorsDataQueries = useQueries(
queryPayloads.map((payload) => ({
queryKey: [
REACT_QUERY_KEY.GET_TOP_ERRORS_BY_DOMAIN,
payload,
DEFAULT_ENTITY_VERSION,
],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, DEFAULT_ENTITY_VERSION),
enabled: !!payload,
staleTime: 60 * 1000, // 1 minute stale time : optimize this part
})),
);
const topErrorsDataQuery = topErrorsDataQueries[0];
const {
data: topErrorsData,
isLoading,
isRefetching,
isError,
refetch,
} = topErrorsDataQuery;
const topErrorsColumnsConfig = useMemo(() => getTopErrorsColumnsConfig(), []);
const formattedTopErrorsData = useMemo(
() =>
formatTopErrorsDataForTable(
topErrorsData?.payload?.data?.result as TopErrorsResponseRow[],
),
[topErrorsData],
);
if (isError) {
return (
<div className="all-endpoints-error-state-wrapper">
<ErrorState refetch={refetch} />
</div>
);
}
console.log('uncaught topErrors Data', formattedTopErrorsData);
return (
<div className="all-endpoints-container">
<div className="endpoints-table-container">
<div className="endpoints-table-header">Top Errors</div>
<Table
columns={topErrorsColumnsConfig}
loading={{
spinning: isLoading || isRefetching,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
dataSource={isLoading || isRefetching ? [] : formattedTopErrorsData}
locale={{
emptyText:
isLoading || isRefetching ? null : (
<div className="no-filtered-endpoints-message-container">
<div className="no-filtered-endpoints-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-endpoints-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
scroll={{ x: true }}
tableLayout="fixed"
rowClassName={(_, index): string =>
index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
}
/>
</div>
</div>
);
}
export default TopErrors;

View File

@ -1,9 +1,11 @@
export enum VIEWS {
ALL_ENDPOINTS = 'all_endpoints',
ENDPOINT_DETAILS = 'endpoint_details',
TOP_ERRORS = 'top_errors',
}
export const VIEW_TYPES = {
ALL_ENDPOINTS: VIEWS.ALL_ENDPOINTS,
ENDPOINT_DETAILS: VIEWS.ENDPOINT_DETAILS,
TOP_ERRORS: VIEWS.TOP_ERRORS,
};

View File

@ -633,6 +633,158 @@ export const getEndPointsQueryPayload = (
];
};
export const getTopErrorsQueryPayload = (
domainName: string,
start: number,
end: number,
): GetQueryResultsProps[] => [
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TABLE,
query: {
builder: {
queryData: [
{
dataSource: DataSource.TRACES,
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
id: '------false',
dataType: DataTypes.String,
key: '',
isColumn: false,
type: '',
isJSON: false,
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters: {
op: 'AND',
items: [
{
id: '04da97bd',
key: {
key: 'kind_string',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
id: 'kind_string--string----true',
},
op: '=',
value: 'Client',
},
{
id: '75d65388',
key: {
key: 'status_message',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
id: 'status_message--string----true',
},
op: 'exists',
value: '',
},
{
id: 'b1af6bdb',
key: {
key: 'http.url',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: 'http.url--string--tag--false',
},
op: 'exists',
value: '',
},
{
id: '4872bf91',
key: {
key: 'net.peer.name',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: 'net.peer.name--string--tag--false',
},
op: '=',
value: domainName,
},
],
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: 10,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
groupBy: [
{
key: 'http.url',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: 'http.url--string--tag--false',
},
{
key: 'status_code',
dataType: DataTypes.Float64,
type: '',
isColumn: true,
isJSON: false,
id: 'status_code--float64----true',
},
{
key: 'status_message',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
id: 'status_message--string----true',
},
],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: '315b15fa-ff0c-442f-89f8-2bf4fb1af2f2',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
variables: {},
start,
end,
step: 240,
},
];
export interface EndPointsTableRowData {
key: string;
endpointName: string;
@ -911,6 +1063,98 @@ export const formatEndPointsDataForTable = (
return formattedData;
};
export interface TopErrorsResponseRow {
metric: {
'http.url': string;
status_code: string;
status_message: string;
};
values: [number, string][];
queryName: string;
legend: string;
}
export interface TopErrorsTableRowData {
key: string;
endpointName: string;
statusCode: string;
statusMessage: string;
count: number | string;
}
export const formatTopErrorsDataForTable = (
data: TopErrorsResponseRow[] | undefined,
): TopErrorsTableRowData[] => {
if (!data) return [];
return data.map((row) => ({
key: v4(),
endpointName:
row.metric['http.url'] === 'n/a' || row.metric['http.url'] === undefined
? '-'
: row.metric['http.url'],
statusCode:
row.metric.status_code === 'n/a' || row.metric.status_code === undefined
? '-'
: row.metric.status_code,
statusMessage:
row.metric.status_message === 'n/a' ||
row.metric.status_message === undefined
? '-'
: row.metric.status_message,
count:
row.values &&
row.values[0] &&
row.values[0][1] !== undefined &&
row.values[0][1] !== 'n/a'
? row.values[0][1]
: '-',
}));
};
export const getTopErrorsColumnsConfig = (): ColumnType<TopErrorsTableRowData>[] => [
{
title: <div className="endpoint-name-header">Endpoint</div>,
dataIndex: 'endpointName',
key: 'endpointName',
width: 180,
ellipsis: true,
sorter: false,
className: 'column',
render: (text: string, record: TopErrorsTableRowData): React.ReactNode => (
<div className="endpoint-name-value">{record.endpointName}</div>
),
},
{
title: <div className="column-header">Status code</div>,
dataIndex: 'statusCode',
key: 'statusCode',
width: 180,
ellipsis: true,
sorter: false,
align: 'right',
className: `column`,
},
{
title: <div className="column-header">Status message</div>,
dataIndex: 'statusMessage',
key: 'statusMessage',
width: 180,
ellipsis: true,
align: 'right',
className: `column`,
},
{
title: <div>Count</div>,
dataIndex: 'count',
key: 'count',
width: 120,
sorter: false,
align: 'right',
className: `column`,
},
];
export const createFiltersForSelectedRowData = (
selectedRowData: EndPointsTableRowData,
currentFilters?: IBuilderQuery['filters'],