From 5424c7714f8a306d48b6b700d0c304d4829fe849 Mon Sep 17 00:00:00 2001 From: palash-signoz Date: Fri, 22 Apr 2022 20:03:08 +0530 Subject: [PATCH] feat: Error exception (#979) * feat: error expection page is made --- .../public/locales/en-GB/errorDetails.json | 7 + .../public/locales/en-GB/translation.json | 3 +- frontend/public/locales/en/errorDetails.json | 7 + frontend/public/locales/en/translation.json | 3 +- frontend/src/AppRoutes/pageComponents.ts | 8 + frontend/src/AppRoutes/routes.ts | 12 ++ frontend/src/api/errors/getAll.ts | 30 ++++ .../api/errors/getByErrorTypeAndService.ts | 32 ++++ frontend/src/api/errors/getById.ts | 31 ++++ frontend/src/components/Editor/index.tsx | 9 +- frontend/src/constants/routes.ts | 2 + frontend/src/container/AllError/index.tsx | 104 ++++++++++++ frontend/src/container/ErrorDetails/index.tsx | 157 ++++++++++++++++++ frontend/src/container/ErrorDetails/styles.ts | 28 ++++ .../container/Header/Breadcrumbs/index.tsx | 1 + frontend/src/container/SideNav/menuItems.ts | 6 + frontend/src/pages/AllErrors/index.tsx | 26 +++ frontend/src/pages/ErrorDetails/index.tsx | 88 ++++++++++ frontend/src/types/api/errors/getAll.ts | 17 ++ .../api/errors/getByErrorTypeAndService.ts | 22 +++ frontend/src/types/api/errors/getById.ts | 11 ++ 21 files changed, 600 insertions(+), 4 deletions(-) create mode 100644 frontend/public/locales/en-GB/errorDetails.json create mode 100644 frontend/public/locales/en/errorDetails.json create mode 100644 frontend/src/api/errors/getAll.ts create mode 100644 frontend/src/api/errors/getByErrorTypeAndService.ts create mode 100644 frontend/src/api/errors/getById.ts create mode 100644 frontend/src/container/AllError/index.tsx create mode 100644 frontend/src/container/ErrorDetails/index.tsx create mode 100644 frontend/src/container/ErrorDetails/styles.ts create mode 100644 frontend/src/pages/AllErrors/index.tsx create mode 100644 frontend/src/pages/ErrorDetails/index.tsx create mode 100644 frontend/src/types/api/errors/getAll.ts create mode 100644 frontend/src/types/api/errors/getByErrorTypeAndService.ts create mode 100644 frontend/src/types/api/errors/getById.ts diff --git a/frontend/public/locales/en-GB/errorDetails.json b/frontend/public/locales/en-GB/errorDetails.json new file mode 100644 index 0000000000..f29f4993c8 --- /dev/null +++ b/frontend/public/locales/en-GB/errorDetails.json @@ -0,0 +1,7 @@ +{ + "see_trace_graph": "See what happened before and after this error in a trace graph", + "see_error_in_trace_graph": "See the error in trace graph", + "stack_trace": "Stacktrace", + "older": "Older", + "newer": "Newer" +} diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 23330080e6..20b33dc2d4 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -11,7 +11,8 @@ "n_a": "N/A", "routes": { "general": "General", - "alert_channels": "Alert Channels" + "alert_channels": "Alert Channels", + "all_errors": "All Errors" }, "settings": { "total_retention_period": "Total Retention Period", diff --git a/frontend/public/locales/en/errorDetails.json b/frontend/public/locales/en/errorDetails.json new file mode 100644 index 0000000000..f29f4993c8 --- /dev/null +++ b/frontend/public/locales/en/errorDetails.json @@ -0,0 +1,7 @@ +{ + "see_trace_graph": "See what happened before and after this error in a trace graph", + "see_error_in_trace_graph": "See the error in trace graph", + "stack_trace": "Stacktrace", + "older": "Older", + "newer": "Newer" +} diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 23330080e6..20b33dc2d4 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -11,7 +11,8 @@ "n_a": "N/A", "routes": { "general": "General", - "alert_channels": "Alert Channels" + "alert_channels": "Alert Channels", + "all_errors": "All Errors" }, "settings": { "total_retention_period": "Total Retention Period", diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index 25d4dc5886..8bc37dcac2 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -86,6 +86,14 @@ export const AllAlertChannels = Loadable( () => import(/* webpackChunkName: "All Channels" */ 'pages/AllAlertChannels'), ); +export const AllErrors = Loadable( + /* webpackChunkName: "All Errors" */ () => import('pages/AllErrors'), +); + +export const ErrorDetails = Loadable( + () => import(/* webpackChunkName: "Error Details" */ 'pages/ErrorDetails'), +); + export const StatusPage = Loadable( () => import(/* webpackChunkName: "All Status" */ 'pages/Status'), ); diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index 10f5a1997a..4ac7a3e116 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -4,11 +4,13 @@ import { RouteProps } from 'react-router-dom'; import { AllAlertChannels, + AllErrors, CreateAlertChannelAlerts, CreateNewAlerts, DashboardPage, EditAlertChannelsAlerts, EditRulesPage, + ErrorDetails, InstrumentationPage, ListAllALertsPage, NewDashboardPage, @@ -114,6 +116,16 @@ const routes: AppRoutes[] = [ exact: true, component: AllAlertChannels, }, + { + path: ROUTES.ALL_ERROR, + exact: true, + component: AllErrors, + }, + { + path: ROUTES.ERROR_DETAIL, + exact: true, + component: ErrorDetails, + }, { path: ROUTES.VERSION, exact: true, diff --git a/frontend/src/api/errors/getAll.ts b/frontend/src/api/errors/getAll.ts new file mode 100644 index 0000000000..6de78faf17 --- /dev/null +++ b/frontend/src/api/errors/getAll.ts @@ -0,0 +1,30 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import createQueryParams from 'lib/createQueryParams'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/errors/getAll'; + +const getAll = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get( + `/errors?${createQueryParams({ + start: props.start.toString(), + end: props.end.toString(), + })}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.message, + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getAll; diff --git a/frontend/src/api/errors/getByErrorTypeAndService.ts b/frontend/src/api/errors/getByErrorTypeAndService.ts new file mode 100644 index 0000000000..6a2c6964d9 --- /dev/null +++ b/frontend/src/api/errors/getByErrorTypeAndService.ts @@ -0,0 +1,32 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import createQueryParams from 'lib/createQueryParams'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/errors/getByErrorTypeAndService'; + +const getByErrorType = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get( + `/errorWithType?${createQueryParams({ + start: props.start.toString(), + end: props.end.toString(), + serviceName: props.serviceName, + errorType: props.errorType, + })}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.message, + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getByErrorType; diff --git a/frontend/src/api/errors/getById.ts b/frontend/src/api/errors/getById.ts new file mode 100644 index 0000000000..3ab7c4aa60 --- /dev/null +++ b/frontend/src/api/errors/getById.ts @@ -0,0 +1,31 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import createQueryParams from 'lib/createQueryParams'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/errors/getById'; + +const getById = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get( + `/errorWithId?${createQueryParams({ + start: props.start.toString(), + end: props.end.toString(), + errorId: props.errorId, + })}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.message, + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getById; diff --git a/frontend/src/components/Editor/index.tsx b/frontend/src/components/Editor/index.tsx index 51a024f607..1dc78d2f32 100644 --- a/frontend/src/components/Editor/index.tsx +++ b/frontend/src/components/Editor/index.tsx @@ -1,13 +1,13 @@ import MEditor from '@monaco-editor/react'; import React from 'react'; -function Editor({ value }: EditorProps): JSX.Element { +function Editor({ value, readOnly = false }: EditorProps): JSX.Element { return ( { if (value.current && newValue) { @@ -21,6 +21,11 @@ function Editor({ value }: EditorProps): JSX.Element { interface EditorProps { value: React.MutableRefObject; + readOnly?: boolean; } +Editor.defaultProps = { + readOnly: false, +}; + export default Editor; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index e62cec59f2..4663ced750 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -17,6 +17,8 @@ const ROUTES = { ALL_CHANNELS: '/settings/channels', CHANNELS_NEW: '/setting/channels/new', CHANNELS_EDIT: '/setting/channels/edit/:id', + ALL_ERROR: '/errors', + ERROR_DETAIL: '/errors/:serviceName/:errorType', VERSION: '/status', }; diff --git a/frontend/src/container/AllError/index.tsx b/frontend/src/container/AllError/index.tsx new file mode 100644 index 0000000000..82a62916da --- /dev/null +++ b/frontend/src/container/AllError/index.tsx @@ -0,0 +1,104 @@ +import { Table, Typography } from 'antd'; +import { ColumnsType } from 'antd/lib/table'; +import getAll from 'api/errors/getAll'; +import ROUTES from 'constants/routes'; +import dayjs from 'dayjs'; +import React from 'react'; +import { useQuery } from 'react-query'; +import { useSelector } from 'react-redux'; +import { generatePath, Link } from 'react-router-dom'; +import { AppState } from 'store/reducers'; +import { Exception } from 'types/api/errors/getAll'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +function AllErrors(): JSX.Element { + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); + + const { isLoading, data } = useQuery(['getAllError', [maxTime, minTime]], { + queryFn: () => + getAll({ + end: maxTime, + start: minTime, + }), + }); + + const getDateValue = (value: string): JSX.Element => { + return ( + {dayjs(value).format('DD/MM/YYYY HH:mm:ss A')} + ); + }; + + const columns: ColumnsType = [ + { + title: 'Exception Type', + dataIndex: 'exceptionType', + key: 'exceptionType', + render: (value, record): JSX.Element => ( + + {value} + + ), + sorter: (a, b): number => a.exceptionType.length - b.exceptionType.length, + }, + { + title: 'Error Message', + dataIndex: 'exceptionMessage', + key: 'exceptionMessage', + render: (value): JSX.Element => ( + + {value} + + ), + }, + { + title: 'Count', + dataIndex: 'exceptionCount', + key: 'exceptionCount', + sorter: (a, b): number => a.exceptionCount - b.exceptionCount, + }, + { + title: 'Last Seen', + dataIndex: 'lastSeen', + key: 'lastSeen', + render: getDateValue, + sorter: (a, b): number => + dayjs(a.lastSeen).isBefore(dayjs(b.lastSeen)) === true ? 1 : 0, + }, + { + title: 'First Seen', + dataIndex: 'firstSeen', + key: 'firstSeen', + render: getDateValue, + sorter: (a, b): number => + dayjs(a.firstSeen).isBefore(dayjs(b.firstSeen)) === true ? 1 : 0, + }, + { + title: 'Application', + dataIndex: 'serviceName', + key: 'serviceName', + sorter: (a, b): number => a.serviceName.length - b.serviceName.length, + }, + ]; + + return ( + + ); +} + +export default AllErrors; diff --git a/frontend/src/container/ErrorDetails/index.tsx b/frontend/src/container/ErrorDetails/index.tsx new file mode 100644 index 0000000000..cdb9800620 --- /dev/null +++ b/frontend/src/container/ErrorDetails/index.tsx @@ -0,0 +1,157 @@ +import { Button, Divider, notification, Space, Table, Typography } from 'antd'; +import Editor from 'components/Editor'; +import dayjs from 'dayjs'; +import history from 'lib/history'; +import React, { useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocation } from 'react-router-dom'; +import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService'; +import { PayloadProps } from 'types/api/errors/getById'; + +import { DashedContainer, EditorContainer, EventContainer } from './styles'; + +function ErrorDetails(props: ErrorDetailsProps): JSX.Element { + const { idPayload } = props; + const [isLoading, setLoading] = useState(false); + const { t } = useTranslation(['errorDetails', 'common']); + + const { search } = useLocation(); + const params = new URLSearchParams(search); + const queryErrorId = params.get('errorId'); + + const errorDetail = idPayload; + + const stackTraceValue = useRef(errorDetail.excepionStacktrace); + + const columns = useMemo( + () => [ + { + title: 'Key', + dataIndex: 'key', + key: 'key', + }, + { + title: 'Value', + dataIndex: 'value', + key: 'value', + }, + ], + [], + ); + + const keyToExclude = useMemo( + () => [ + 'excepionStacktrace', + 'exceptionType', + 'errorId', + 'timestamp', + 'exceptionMessage', + 'newerErrorId', + 'olderErrorId', + ], + [], + ); + + const onClickErrorIdHandler = async (id: string): Promise => { + try { + setLoading(true); + + if (id.length === 0) { + notification.error({ + message: 'Error Id cannot be empty', + }); + setLoading(false); + return; + } + + history.push(`${history.location.pathname}?errorId=${id}`); + + setLoading(false); + } catch (error) { + notification.error({ + message: t('something_went_wrong'), + }); + setLoading(false); + } + }; + + const timeStamp = dayjs(errorDetail.timestamp); + + const data: { key: string; value: string }[] = Object.keys(errorDetail) + .filter((e) => !keyToExclude.includes(e)) + .map((key) => ({ + key, + value: errorDetail[key as keyof GetByErrorTypeAndServicePayload], + })); + + const onClickTraceHandler = (): void => { + history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`); + }; + + return ( + <> + {errorDetail.exceptionType} + {errorDetail.exceptionMessage} + + + +
+ Event {errorDetail.errorId} + {timeStamp.format('MMM DD YYYY hh:mm:ss A')} +
+
+ + {/* + + {/*
+
+ + + {t('see_trace_graph')} + + + + {t('stack_trace')} + + + + +
+ + + + ); +} + +interface ErrorDetailsProps { + idPayload: PayloadProps; +} + +export default ErrorDetails; diff --git a/frontend/src/container/ErrorDetails/styles.ts b/frontend/src/container/ErrorDetails/styles.ts new file mode 100644 index 0000000000..d1cd0327a5 --- /dev/null +++ b/frontend/src/container/ErrorDetails/styles.ts @@ -0,0 +1,28 @@ +import { grey } from '@ant-design/colors'; +import styled from 'styled-components'; + +export const DashedContainer = styled.div` + border: ${`1px dashed ${grey[0]}`}; + box-sizing: border-box; + border-radius: 0.25rem; + display: flex; + justify-content: space-between; + padding: 1rem; + margin-top: 1.875rem; + margin-bottom: 1.625rem; + align-items: center; +`; + +export const ButtonContainer = styled.div` + display: flex; + gap: 1rem; +`; + +export const EventContainer = styled.div` + display: flex; + justify-content: space-between; +`; + +export const EditorContainer = styled.div` + margin-top: 1.5rem; +`; diff --git a/frontend/src/container/Header/Breadcrumbs/index.tsx b/frontend/src/container/Header/Breadcrumbs/index.tsx index d88384b75c..362122d2fa 100644 --- a/frontend/src/container/Header/Breadcrumbs/index.tsx +++ b/frontend/src/container/Header/Breadcrumbs/index.tsx @@ -11,6 +11,7 @@ const breadcrumbNameMap = { [ROUTES.INSTRUMENTATION]: 'Add instrumentation', [ROUTES.SETTINGS]: 'Settings', [ROUTES.DASHBOARD]: 'Dashboard', + [ROUTES.ALL_ERROR]: 'Errors', [ROUTES.VERSION]: 'Status', }; diff --git a/frontend/src/container/SideNav/menuItems.ts b/frontend/src/container/SideNav/menuItems.ts index 1e1cb0c49e..5fcd630e2b 100644 --- a/frontend/src/container/SideNav/menuItems.ts +++ b/frontend/src/container/SideNav/menuItems.ts @@ -3,6 +3,7 @@ import { AlignLeftOutlined, ApiOutlined, BarChartOutlined, + BugOutlined, DashboardFilled, DeploymentUnitOutlined, LineChartOutlined, @@ -31,6 +32,11 @@ const menus: SidebarMenu[] = [ to: ROUTES.LIST_ALL_ALERT, name: 'Alerts', }, + { + Icon: BugOutlined, + to: ROUTES.ALL_ERROR, + name: 'Errors', + }, { to: ROUTES.SERVICE_MAP, name: 'Service Map', diff --git a/frontend/src/pages/AllErrors/index.tsx b/frontend/src/pages/AllErrors/index.tsx new file mode 100644 index 0000000000..4093a20657 --- /dev/null +++ b/frontend/src/pages/AllErrors/index.tsx @@ -0,0 +1,26 @@ +import RouteTab from 'components/RouteTab'; +import ROUTES from 'constants/routes'; +import AllErrorsContainer from 'container/AllError'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +function AllErrors(): JSX.Element { + const { t } = useTranslation(); + + return ( + + ); +} + +export default AllErrors; diff --git a/frontend/src/pages/ErrorDetails/index.tsx b/frontend/src/pages/ErrorDetails/index.tsx new file mode 100644 index 0000000000..16f2f212a5 --- /dev/null +++ b/frontend/src/pages/ErrorDetails/index.tsx @@ -0,0 +1,88 @@ +import { Typography } from 'antd'; +import getByErrorType from 'api/errors/getByErrorTypeAndService'; +import getById from 'api/errors/getById'; +import Spinner from 'components/Spinner'; +import ErrorDetailsContainer from 'container/ErrorDetails'; +import React from 'react'; +import { useQuery } from 'react-query'; +import { useSelector } from 'react-redux'; +import { useLocation, useParams } from 'react-router-dom'; +import { AppState } from 'store/reducers'; +import { PayloadProps } from 'types/api/errors/getById'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +function ErrorDetails(): JSX.Element { + const { errorType, serviceName } = useParams(); + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); + const { search } = useLocation(); + const params = new URLSearchParams(search); + + const errorId = params.get('errorId'); + + const { data, status } = useQuery( + [ + 'errorByType', + errorType, + 'serviceName', + serviceName, + maxTime, + minTime, + errorId, + ], + { + queryFn: () => + getByErrorType({ + end: maxTime, + errorType, + serviceName, + start: minTime, + }), + enabled: errorId === null, + cacheTime: 5000, + }, + ); + + const { status: ErrorIdStatus, data: errorIdPayload } = useQuery( + [ + 'errorByType', + errorType, + 'serviceName', + serviceName, + maxTime, + minTime, + 'errorId', + errorId, + ], + { + queryFn: () => + getById({ + end: maxTime, + errorId: errorId || data?.payload?.errorId || '', + start: minTime, + }), + enabled: errorId !== null || status === 'success', + cacheTime: 5000, + }, + ); + + if (status === 'loading' || ErrorIdStatus === 'loading') { + return ; + } + + if (status === 'error' || ErrorIdStatus === 'error') { + return {data?.error || errorIdPayload?.error}; + } + + return ( + + ); +} + +export interface ErrorDetailsParams { + errorType: string; + serviceName: string; +} + +export default ErrorDetails; diff --git a/frontend/src/types/api/errors/getAll.ts b/frontend/src/types/api/errors/getAll.ts new file mode 100644 index 0000000000..98c3122f7d --- /dev/null +++ b/frontend/src/types/api/errors/getAll.ts @@ -0,0 +1,17 @@ +import { GlobalTime } from 'types/actions/globalTime'; + +export interface Props { + start: GlobalTime['minTime']; + end: GlobalTime['maxTime']; +} + +export interface Exception { + exceptionType: string; + exceptionMessage: string; + exceptionCount: number; + lastSeen: string; + firstSeen: string; + serviceName: string; +} + +export type PayloadProps = Exception[]; diff --git a/frontend/src/types/api/errors/getByErrorTypeAndService.ts b/frontend/src/types/api/errors/getByErrorTypeAndService.ts new file mode 100644 index 0000000000..0072d1b2d6 --- /dev/null +++ b/frontend/src/types/api/errors/getByErrorTypeAndService.ts @@ -0,0 +1,22 @@ +import { GlobalTime } from 'types/actions/globalTime'; + +export interface Props { + start: GlobalTime['minTime']; + end: GlobalTime['maxTime']; + serviceName: string; + errorType: string; +} + +export interface PayloadProps { + errorId: string; + exceptionType: string; + excepionStacktrace: string; + exceptionEscaped: string; + exceptionMessage: string; + timestamp: string; + spanID: string; + traceID: string; + serviceName: Props['serviceName']; + newerErrorId: string; + olderErrorId: string; +} diff --git a/frontend/src/types/api/errors/getById.ts b/frontend/src/types/api/errors/getById.ts new file mode 100644 index 0000000000..c812410b89 --- /dev/null +++ b/frontend/src/types/api/errors/getById.ts @@ -0,0 +1,11 @@ +import { GlobalTime } from 'types/actions/globalTime'; + +import { PayloadProps as Payload } from './getByErrorTypeAndService'; + +export type PayloadProps = Payload; + +export type Props = { + start: GlobalTime['minTime']; + end: GlobalTime['minTime']; + errorId: string; +};