Skip to content
Snippets Groups Projects
Commit ebe74aac authored by Piotr Gawron's avatar Piotr Gawron
Browse files

provide stacktrace in report and remove cyclic store dependency

parent 2f3c902a
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!199Resolve "[MIN-321] form for reporting errors in minerva"
Showing
with 147 additions and 53 deletions
......@@ -3,9 +3,6 @@ import type { AppProps } from 'next/app';
import '@/styles/index.css';
import { useEffect } from 'react';
import { store } from '@/redux/store';
import { initializeErrorReporting } from '@/utils/error-report/errorReporting';
initializeErrorReporting();
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
const project = store.getState().project.data;
......
......@@ -6,6 +6,7 @@ import { Publication, PublicationsResponse } from '@/types/models';
import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
import { getError } from '@/utils/error-report/getError';
import { handleError } from '@/utils/error-report/errorReporting';
import { store } from '@/redux/store';
interface Args {
length: number;
......@@ -21,7 +22,10 @@ export const getBasePublications = async ({ length }: Args): Promise<Publication
return isDataValid ? response.data.data : [];
} catch (error) {
handleError(getError({ error, prefix: PUBLICATIONS_FETCHING_ERROR_PREFIX }));
await handleError(
getError({ error, prefix: PUBLICATIONS_FETCHING_ERROR_PREFIX }),
store.getState(),
);
return [];
}
};
......@@ -7,6 +7,7 @@ import { BioEntityContent, BioEntityResponse } from '@/types/models';
import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
import { getError } from '@/utils/error-report/getError';
import { handleError } from '@/utils/error-report/errorReporting';
import { store } from '@/redux/store';
export const fetchElementData = async (
searchQuery: string,
......@@ -25,7 +26,10 @@ export const fetchElementData = async (
return response.data.content[FIRST_ARRAY_ELEMENT];
}
} catch (error) {
handleError(getError({ error, prefix: BIO_ENTITY_FETCHING_ERROR_PREFIX }));
await handleError(
getError({ error, prefix: BIO_ENTITY_FETCHING_ERROR_PREFIX }),
store.getState(),
);
}
return undefined;
......
......@@ -4,6 +4,7 @@ import { ChangeEvent, useMemo, useState, KeyboardEvent } from 'react';
import { ENTER_KEY_CODE } from '@/constants/common';
import { getError } from '@/utils/error-report/getError';
import { handleError } from '@/utils/error-report/errorReporting';
import { store } from '@/redux/store';
import { PLUGIN_LOADING_ERROR_PREFIX } from '../../AvailablePluginsDrawer.constants';
type UseLoadPluginReturnType = {
......@@ -43,7 +44,7 @@ export const useLoadPluginFromUrl = (): UseLoadPluginReturnType => {
setPluginUrl('');
} catch (error) {
handleError(getError({ error, prefix: PLUGIN_LOADING_ERROR_PREFIX }));
await handleError(getError({ error, prefix: PLUGIN_LOADING_ERROR_PREFIX }), store.getState());
} finally {
setIsLoading(false);
}
......
import { ZOD_SEED } from '@/constants';
// eslint-disable-next-line import/no-extraneous-dependencies
import { createFixture } from 'zod-fixture';
import { javaStacktraceSchema } from '@/models/javaStacktraceSchema';
export const javaStacktraceFixture = createFixture(javaStacktraceSchema, {
seed: ZOD_SEED,
});
import { z } from 'zod';
export const javaStacktraceSchema = z.object({
id: z.string(),
content: z.string(),
createdAt: z.string(),
});
......@@ -94,4 +94,5 @@ export const apiPath = {
getSubmapConnections: (): string => `projects/${PROJECT_ID}/submapConnections/`,
logout: (): string => `doLogout`,
user: (login: string): string => `users/${login}`,
getStacktrace: (code: string): string => `stacktrace/${code}`,
};
import { handleError } from '@/utils/error-report/errorReporting';
import { store } from '@/redux/store';
import { errorMiddlewareListener } from './error.middleware';
jest.mock('../../utils/error-report/errorReporting', () => ({
......@@ -25,8 +26,11 @@ describe('errorMiddlewareListener', () => {
code: 'Error 2',
},
};
await errorMiddlewareListener(action);
expect(handleError).toHaveBeenCalledWith({ code: 'Error 2' });
const { getState } = store;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
await errorMiddlewareListener(action, { getState });
expect(handleError).toHaveBeenCalledWith({ code: 'Error 2' }, getState());
});
it('should handle error without message when action is rejected without value', async () => {
......@@ -42,8 +46,11 @@ describe('errorMiddlewareListener', () => {
code: 'Error 3',
},
};
await errorMiddlewareListener(action);
expect(handleError).toHaveBeenCalledWith({ code: 'Error 3' });
const { getState } = store;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
await errorMiddlewareListener(action, { getState });
expect(handleError).toHaveBeenCalledWith({ code: 'Error 3' }, getState());
});
it('should not handle error when action is not rejected', async () => {
......@@ -55,7 +62,10 @@ describe('errorMiddlewareListener', () => {
requestStatus: 'fulfilled',
},
};
await errorMiddlewareListener(action);
const { getState } = store;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
await errorMiddlewareListener(action, { getState });
expect(handleError).not.toHaveBeenCalled();
});
......@@ -73,8 +83,14 @@ describe('errorMiddlewareListener', () => {
message: 'Error message',
},
};
await errorMiddlewareListener(action);
expect(handleError).toHaveBeenCalledWith({ code: 'ERROR', message: 'Error message' });
const { getState } = store;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
await errorMiddlewareListener(action, { getState });
expect(handleError).toHaveBeenCalledWith(
{ code: 'ERROR', message: 'Error message' },
getState(),
);
});
it('should handle error with custom message when action payload is a string', async () => {
......@@ -91,7 +107,10 @@ describe('errorMiddlewareListener', () => {
message: 'xyz',
},
};
await errorMiddlewareListener(action);
expect(handleError).toHaveBeenCalledWith({ code: 'ERROR', message: 'xyz' });
const { getState } = store;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
await errorMiddlewareListener(action, { getState });
expect(handleError).toHaveBeenCalledWith({ code: 'ERROR', message: 'xyz' }, getState());
});
});
import type { AppStartListening } from '@/redux/store';
import type { AppListenerEffectAPI, AppStartListening } from '@/redux/store';
import { Action, createListenerMiddleware, isRejected } from '@reduxjs/toolkit';
import { handleError } from '@/utils/error-report/errorReporting';
......@@ -6,9 +6,12 @@ export const errorListenerMiddleware = createListenerMiddleware();
const startListening = errorListenerMiddleware.startListening as AppStartListening;
export const errorMiddlewareListener = async (action: Action): Promise<void> => {
export const errorMiddlewareListener = async (
action: Action,
{ getState }: AppListenerEffectAPI,
): Promise<void> => {
if (isRejected(action)) {
handleError(action.error);
await handleError(action.error, getState());
}
};
......
......@@ -62,6 +62,7 @@ import { targetSearchNameResult } from '@/models/targetSearchNameResult';
import { userPrivilegeSchema } from '@/models/userPrivilegesSchema';
import { z } from 'zod';
import { userSchema } from '@/models/userSchema';
import { javaStacktraceSchema } from '@/models/javaStacktraceSchema';
export type Project = z.infer<typeof projectSchema>;
export type OverviewImageView = z.infer<typeof overviewImageView>;
......@@ -120,3 +121,4 @@ export type MarkerSurface = z.infer<typeof markerSurfaceSchema>;
export type MarkerLine = z.infer<typeof markerLineSchema>;
export type MarkerWithPosition = z.infer<typeof markerWithPositionSchema>;
export type Marker = z.infer<typeof markerSchema>;
export type JavaStacktrace = z.infer<typeof javaStacktraceSchema>;
......@@ -8,6 +8,8 @@ import { store } from '@/redux/store';
import { getConfiguration } from '@/redux/configuration/configuration.thunks';
import { configurationFixture } from '@/models/fixtures/configurationFixture';
import { userFixture } from '@/models/fixtures/userFixture';
import { SerializedError } from '@reduxjs/toolkit';
import { javaStacktraceFixture } from '@/models/fixtures/javaStacktraceFixture';
const mockedAxiosClient = mockNetworkResponse();
......@@ -17,23 +19,23 @@ const CREDENTIALS = {
};
describe('createErrorData', () => {
it('should add stacktrace', () => {
const error = createErrorData(new Error('hello'));
it('should add stacktrace', async () => {
const error = await createErrorData(new Error('hello'), store.getState());
expect(error.stacktrace).not.toEqual('');
});
it('should add url', () => {
const error = createErrorData(new Error('hello'));
it('should add url', async () => {
const error = await createErrorData(new Error('hello'), store.getState());
expect(error.url).not.toBeNull();
});
it('should add browser', () => {
const error = createErrorData(new Error('hello'));
it('should add browser', async () => {
const error = await createErrorData(new Error('hello'), store.getState());
expect(error.browser).not.toBeNull();
});
it('should add guest login when not logged', () => {
const error = createErrorData(new Error('hello'));
it('should add guest login when not logged', async () => {
const error = await createErrorData(new Error('hello'), store.getState());
expect(error.login).toBe('anonymous');
});
......@@ -42,13 +44,13 @@ describe('createErrorData', () => {
mockedAxiosClient.onGet(apiPath.user(loginFixture.login)).reply(HttpStatusCode.Ok, userFixture);
await store.dispatch(login(CREDENTIALS));
const error = createErrorData(new Error('hello'));
const error = await createErrorData(new Error('hello'), store.getState());
expect(error.login).not.toBe('anonymous');
expect(error.login).toBe(loginFixture.login);
});
it('should add timestamp', () => {
const error = createErrorData(new Error());
it('should add timestamp', async () => {
const error = await createErrorData(new Error(), store.getState());
expect(error.timestamp).not.toBeNull();
});
......@@ -58,7 +60,7 @@ describe('createErrorData', () => {
.reply(HttpStatusCode.Ok, configurationFixture);
await store.dispatch(getConfiguration());
const error = createErrorData(new Error());
const error = await createErrorData(new Error(), store.getState());
expect(error.version).not.toBeNull();
});
......@@ -67,14 +69,26 @@ describe('createErrorData', () => {
mockedAxiosClient.onGet(apiPath.user(loginFixture.login)).reply(HttpStatusCode.Ok, userFixture);
await store.dispatch(login(CREDENTIALS));
const error = createErrorData(new Error());
const error = await createErrorData(new Error(), store.getState());
expect(error.email).toBe(userFixture.email);
});
it('email should be empty when not logged', async () => {
mockedAxiosClient.onPost(apiPath.logout()).reply(HttpStatusCode.Ok, {});
await store.dispatch(logout());
const error = createErrorData(new Error());
const error = await createErrorData(new Error(), store.getState());
expect(error.email).toBeNull();
});
it('java stacktrace should be attached if error report provides info', async () => {
mockedAxiosClient
.onGet(apiPath.getStacktrace('dab932be-1e2e-45d7-b57a-aff30e2629e6'))
.reply(HttpStatusCode.Ok, javaStacktraceFixture);
const error: SerializedError = {
code: 'dab932be-1e2e-45d7-b57a-aff30e2629e6',
};
const errorData = await createErrorData(error, store.getState());
expect(errorData.javaStacktrace).not.toBeNull();
});
});
import { ErrorData } from '@/utils/error-report/ErrorData';
import { SerializedError } from '@reduxjs/toolkit';
// eslint-disable-next-line import/no-cycle
import { store } from '@/redux/store';
import { ONE_THOUSAND } from '@/constants/common';
import {
UNKNOWN_AXIOS_ERROR_CODE,
UNKNOWN_ERROR,
} from '@/utils/getErrorMessage/getErrorMessage.constants';
import { axiosInstance } from '@/services/api/utils/axiosInstance';
import { JavaStacktrace } from '@/types/models';
import { apiPath } from '@/redux/apiPath';
import type { RootState } from '@/redux/store';
export const createErrorData = (error: Error | SerializedError | undefined): ErrorData => {
export const createErrorData = async (
error: Error | SerializedError | undefined,
state: RootState,
): Promise<ErrorData> => {
let stacktrace = '';
if (error !== undefined) {
stacktrace = error.stack !== undefined ? error.stack : '';
}
let { login } = store.getState().user;
let login = null;
let userData = null;
if (state.user) {
login = state.user.login;
userData = state.user.userData;
}
if (!login) {
login = 'anonymous';
}
const { userData } = store.getState().user;
let email = null;
if (userData) {
email = userData.email;
}
const configuration = store.getState().configuration.main.data;
const configuration = state?.configuration?.main?.data;
const version = configuration ? configuration.version : null;
let javaStacktrace = null;
if (error !== undefined && 'code' in error) {
const { code } = error;
if (code && code !== UNKNOWN_ERROR && code !== UNKNOWN_AXIOS_ERROR_CODE) {
try {
javaStacktrace = (await axiosInstance.get<JavaStacktrace>(apiPath.getStacktrace(code))).data
.content;
} catch (e) {
// eslint-disable-next-line no-console
console.log('Problem with fetching javaStacktrace', e);
}
}
}
return {
url: window.location.href,
url: window?.location?.href,
login,
browser: navigator.userAgent,
comment: null,
email,
javaStacktrace: null, // TODO
javaStacktrace,
stacktrace,
timestamp: Math.floor(+new Date() / ONE_THOUSAND),
version,
};
};
export const handleError = (error: Error | SerializedError | undefined): void => {
const errorData = createErrorData(error);
export const handleError = async (
error: Error | SerializedError | undefined,
state: RootState,
): Promise<void> => {
const errorData = await createErrorData(error, state);
// eslint-disable-next-line no-console
console.log(errorData);
};
export const initializeErrorReporting = (): void => {
if (typeof window !== 'undefined') {
window.onerror = (msg, url, lineNo, columnNo, error): boolean => {
handleError(error);
return true;
};
}
};
......@@ -6,8 +6,19 @@ import {
export const getErrorCode = (error: unknown): string => {
if (axios.isAxiosError(error)) {
const { code } = error;
return code || UNKNOWN_AXIOS_ERROR_CODE;
let code = UNKNOWN_AXIOS_ERROR_CODE;
try {
if (error.response) {
if (typeof error.response.data === 'object') {
code = error.response.data['error-id'];
} else if (typeof error.response.data === 'string') {
code = JSON.parse(error.response.data)['error-id'];
}
}
} catch (e) {
code = UNKNOWN_AXIOS_ERROR_CODE;
}
return code;
}
return UNKNOWN_ERROR;
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment