diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/index.tsx b/x-pack/plugins/security_solution/public/common/components/toasters/index.tsx
index b9dd782d8a653..ff3d575df5686 100644
--- a/x-pack/plugins/security_solution/public/common/components/toasters/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/toasters/index.tsx
@@ -20,6 +20,7 @@ export * from './errors';
* @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead
*/
export interface AppToast extends Toast {
+ // FunFact: In a very rare case of errors this can be something other than array. We have a unit test case for it and am leaving it like this type for now.
errors?: string[];
}
diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.test.tsx b/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.test.tsx
index 7ec0553591103..94cf94ed46da7 100644
--- a/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.test.tsx
@@ -54,6 +54,18 @@ describe('Modal all errors', () => {
mockToastWithTwoError.errors.length
);
});
+
+ // This test exists to ensure that errors will work if it is a non-array which can happen in rare corner cases.
+ test('it doesnt cause errors when errors is not an array which can be the rare case in corner cases', () => {
+ const mockToastWithTwoError = cloneDeep(mockToast);
+ mockToastWithTwoError.errors = ('' as unknown) as string[];
+ const wrapper = shallow(
+
+ );
+ expect(wrapper.find('[data-test-subj="modal-all-errors-accordion"]').length).toBe(
+ mockToastWithTwoError.errors.length
+ );
+ });
});
describe('events', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx b/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx
index 0a78139f5fe3a..29058a87a96b5 100644
--- a/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx
@@ -23,12 +23,18 @@ import styled from 'styled-components';
import { AppToast } from '.';
import * as i18n from './translations';
+/**
+ * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead
+ */
interface FullErrorProps {
isShowing: boolean;
toast: AppToast;
toggle: (toast: AppToast) => void;
}
+/**
+ * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead
+ */
const ModalAllErrorsComponent: React.FC = ({ isShowing, toast, toggle }) => {
const handleClose = useCallback(() => toggle(toast), [toggle, toast]);
@@ -43,7 +49,7 @@ const ModalAllErrorsComponent: React.FC = ({ isShowing, toast, t
- {toast.errors != null &&
+ {Array.isArray(toast.errors) && // FunFact: This can be a non-array in some rare cases
toast.errors.map((error, index) => (
= ({ isShowing, toast, t
);
};
+/**
+ * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead
+ */
export const ModalAllErrors = React.memo(ModalAllErrorsComponent);
+/**
+ * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead
+ */
const MyEuiCodeBlock = styled(EuiCodeBlock)`
margin-top: 4px;
`;
+/**
+ * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead
+ */
MyEuiCodeBlock.displayName = 'MyEuiCodeBlock';
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts
index 27f584bb17248..da6b41080c1c7 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts
@@ -9,7 +9,19 @@ import { renderHook } from '@testing-library/react-hooks';
import { IEsError } from 'src/plugins/data/public';
import { useToasts } from '../lib/kibana';
-import { useAppToasts } from './use_app_toasts';
+import { KibanaError, SecurityAppError } from '../utils/api';
+import {
+ appErrorToErrorStack,
+ convertErrorToEnumerable,
+ errorToErrorStack,
+ errorToErrorStackAdapter,
+ esErrorToErrorStack,
+ getStringifiedStack,
+ isEmptyObjectWhenStringified,
+ MaybeESError,
+ unknownToErrorStack,
+ useAppToasts,
+} from './use_app_toasts';
jest.mock('../lib/kibana');
@@ -29,45 +41,449 @@ describe('useAppToasts', () => {
}));
});
- it('works normally with a regular error', async () => {
- const error = new Error('regular error');
- const { result } = renderHook(() => useAppToasts());
+ describe('useAppToasts', () => {
+ it('works normally with a regular error', async () => {
+ const error = new Error('regular error');
+ const { result } = renderHook(() => useAppToasts());
- result.current.addError(error, { title: 'title' });
+ result.current.addError(error, { title: 'title' });
- expect(addErrorMock).toHaveBeenCalledWith(error, { title: 'title' });
+ expect(addErrorMock).toHaveBeenCalledWith(error, { title: 'title' });
+ });
+
+ it('converts an unknown error to an Error', () => {
+ const unknownError = undefined;
+
+ const { result } = renderHook(() => useAppToasts());
+
+ result.current.addError(unknownError, { title: 'title' });
+
+ expect(addErrorMock).toHaveBeenCalledWith(Error(`${undefined}`), {
+ title: 'title',
+ });
+ });
+
+ it("uses a AppError's body.message as the toastMessage", async () => {
+ const kibanaApiError = {
+ message: 'Not Found',
+ body: { status_code: 404, message: 'Detailed Message' },
+ };
+
+ const { result } = renderHook(() => useAppToasts());
+
+ result.current.addError(kibanaApiError, { title: 'title' });
+
+ expect(addErrorMock).toHaveBeenCalledWith(Error('Detailed Message (404)'), {
+ title: 'title',
+ });
+ });
+
+ it("parses AppError's body in the stack trace", async () => {
+ const kibanaApiError = {
+ message: 'Not Found',
+ body: { status_code: 404, message: 'Detailed Message' },
+ };
+
+ const { result } = renderHook(() => useAppToasts());
+
+ result.current.addError(kibanaApiError, { title: 'title' });
+ const errorObj = addErrorMock.mock.calls[0][0];
+ expect(errorObj.name).toEqual('');
+ expect(JSON.parse(errorObj.stack)).toEqual({
+ message: 'Not Found',
+ body: { status_code: 404, message: 'Detailed Message' },
+ });
+ });
+
+ it('works normally with a bsearch type error', async () => {
+ const error = ({
+ message: 'some message',
+ attributes: {}, // empty object and should not show up in the output
+ err: {
+ statusCode: 400,
+ innerMessages: { somethingElse: 'message' },
+ },
+ } as unknown) as IEsError;
+ const { result } = renderHook(() => useAppToasts());
+
+ result.current.addError(error, { title: 'title' });
+ const expected = Error('some message (400)');
+ expect(addErrorMock).toHaveBeenCalledWith(expected, { title: 'title' });
+ });
+
+ it('parses a bsearch correctly in the stack and name', async () => {
+ const error = ({
+ message: 'some message',
+ attributes: {}, // empty object and should not show up in the output
+ err: {
+ statusCode: 400,
+ innerMessages: { somethingElse: 'message' },
+ },
+ } as unknown) as IEsError;
+ const { result } = renderHook(() => useAppToasts());
+ result.current.addError(error, { title: 'title' });
+ const errorObj = addErrorMock.mock.calls[0][0];
+ expect(errorObj.name).toEqual('some message');
+ expect(JSON.parse(errorObj.stack)).toEqual({
+ statusCode: 400,
+ innerMessages: {
+ somethingElse: 'message',
+ },
+ });
+ });
});
- it('converts an unknown error to an Error', () => {
- const unknownError = undefined;
+ describe('errorToErrorStackAdapter', () => {
+ it('works normally with a regular error', async () => {
+ const error = new Error('regular error');
+ const result = errorToErrorStackAdapter(error);
+ expect(result).toEqual(error);
+ });
- const { result } = renderHook(() => useAppToasts());
+ it('has a stack on the error with name, message, and a stack call', async () => {
+ const error = new Error('regular error');
+ const result = errorToErrorStackAdapter(error);
+ const parsedStack = JSON.parse(result.stack ?? '');
+ expect(parsedStack.name).toEqual('Error');
+ expect(parsedStack.message).toEqual('regular error');
+ expect(parsedStack.stack).toEqual(expect.stringContaining('Error: regular error'));
+ });
- result.current.addError(unknownError, { title: 'title' });
+ it('converts an unknown error to an Error', () => {
+ const unknownError = undefined;
+ const result = errorToErrorStackAdapter(unknownError);
+ expect(result).toEqual(Error('undefined'));
+ });
- expect(addErrorMock).toHaveBeenCalledWith(Error(`${undefined}`), {
- title: 'title',
+ it("uses a AppError's body.message", async () => {
+ const kibanaApiError = {
+ message: 'Not Found',
+ body: { status_code: 404, message: 'Detailed Message' },
+ };
+ const result = errorToErrorStackAdapter(kibanaApiError);
+ expect(result).toEqual(Error('Detailed Message (404)'));
+ });
+
+ it("parses AppError's body in the stack trace", async () => {
+ const kibanaApiError = {
+ message: 'Not Found',
+ body: { status_code: 404, message: 'Detailed Message' },
+ };
+ const result = errorToErrorStackAdapter(kibanaApiError);
+ const parsedStack = JSON.parse(result.stack ?? '');
+ expect(parsedStack.message).toEqual('Not Found');
+ expect(parsedStack.body).toEqual({ status_code: 404, message: 'Detailed Message' });
+ });
+
+ it('works normally with a bsearch type error', async () => {
+ const error = ({
+ message: 'some message',
+ attributes: {}, // empty object and should not show up in the output
+ err: {
+ statusCode: 400,
+ innerMessages: { somethingElse: 'message' },
+ },
+ } as unknown) as IEsError;
+ const result = errorToErrorStackAdapter(error);
+ expect(result).toEqual(Error('some message (400)'));
+ });
+
+ it('parses a bsearch correctly in the stack and name', async () => {
+ const error = ({
+ message: 'some message',
+ attributes: {}, // empty object and should not show up in the output
+ err: {
+ statusCode: 400,
+ innerMessages: { somethingElse: 'message' },
+ },
+ } as unknown) as IEsError;
+ const result = errorToErrorStackAdapter(error);
+ const parsedStack = JSON.parse(result.stack ?? '');
+ expect(parsedStack).toEqual({
+ statusCode: 400,
+ innerMessages: {
+ somethingElse: 'message',
+ },
+ });
});
});
- it('works normally with a bsearch type error', async () => {
- const error = ({
- message: 'some message',
- attributes: {},
- err: {
+ describe('esErrorToErrorStack', () => {
+ it('works with a IEsError that is not an EsError', async () => {
+ const error: IEsError = {
+ statusCode: 200,
+ message: 'a message',
+ };
+ const result = esErrorToErrorStack(error);
+ expect(result).toEqual(Error('a message (200)'));
+ });
+
+ it('creates a stack trace of a IEsError that is not an EsError', async () => {
+ const error: IEsError = {
+ statusCode: 200,
+ message: 'a message',
+ };
+ const result = esErrorToErrorStack(error);
+ const parsedStack = JSON.parse(result.stack ?? '');
+ expect(parsedStack).toEqual({ statusCode: 200, message: 'a message' });
+ });
+
+ it('prefers the attributes reason if we have it for the message', async () => {
+ const error: IEsError = {
+ attributes: { type: 'some type', reason: 'message we want' },
+ statusCode: 200,
+ message: 'message we do not want',
+ };
+ const result = esErrorToErrorStack(error);
+ expect(result).toEqual(Error('message we want (200)'));
+ });
+
+ it('works with an EsError, by using the inner error and not outer error if available', async () => {
+ const error: MaybeESError = {
+ attributes: { type: 'some type', reason: 'message we want' },
+ statusCode: 400,
+ err: {
+ statusCode: 200,
+ attributes: { reason: 'attribute message we do not want' },
+ },
+ message: 'main message we do not want',
+ };
+ const result = esErrorToErrorStack(error);
+ expect(result).toEqual(Error('message we want (200)'));
+ });
+
+ it('creates a stack trace of a EsError and not the outer object', async () => {
+ const error: MaybeESError = {
+ attributes: { type: 'some type', reason: 'message we do not want' },
statusCode: 400,
- innerMessages: { somethingElse: 'message' },
- },
- } as unknown) as IEsError;
- const { result } = renderHook(() => useAppToasts());
-
- result.current.addError(error, { title: 'title' });
- const errorObj = addErrorMock.mock.calls[0][0];
- expect(errorObj).toEqual({
- message: 'some message (400)',
- name: 'some message',
- stack:
- '{\n "statusCode": 400,\n "innerMessages": {\n "somethingElse": "message"\n }\n}',
+ err: {
+ statusCode: 200,
+ attributes: { reason: 'attribute message we do want' },
+ },
+ message: 'main message we do not want',
+ };
+ const result = esErrorToErrorStack(error);
+ const parsedStack = JSON.parse(result.stack ?? '');
+ expect(parsedStack).toEqual({
+ statusCode: 200,
+ attributes: { reason: 'attribute message we do want' },
+ });
+ });
+ });
+
+ describe('appErrorToErrorStack', () => {
+ it('works with a AppError that is a KibanaError', async () => {
+ const error: KibanaError = {
+ message: 'message',
+ name: 'some name',
+ body: {
+ message: 'a message',
+ statusCode: 200,
+ },
+ };
+ const result = appErrorToErrorStack(error);
+ expect(result).toEqual(Error('a message (200)'));
+ });
+
+ it('creates a stack trace of a KibanaError', async () => {
+ const error: KibanaError = {
+ message: 'message',
+ name: 'some name',
+ body: {
+ message: 'a message',
+ statusCode: 200,
+ },
+ };
+ const result = appErrorToErrorStack(error);
+ const parsedStack = JSON.parse(result.stack ?? '');
+ expect(parsedStack).toEqual({
+ message: 'message',
+ name: 'some name',
+ body: {
+ message: 'a message',
+ statusCode: 200,
+ },
+ });
+ });
+
+ it('works with a AppError that is a SecurityAppError', async () => {
+ const error: SecurityAppError = {
+ message: 'message',
+ name: 'some name',
+ body: {
+ message: 'a message',
+ status_code: 200,
+ },
+ };
+ const result = appErrorToErrorStack(error);
+ expect(result).toEqual(Error('a message (200)'));
+ });
+
+ it('creates a stack trace of a SecurityAppError', async () => {
+ const error: SecurityAppError = {
+ message: 'message',
+ name: 'some name',
+ body: {
+ message: 'a message',
+ status_code: 200,
+ },
+ };
+ const result = appErrorToErrorStack(error);
+ const parsedStack = JSON.parse(result.stack ?? '');
+ expect(parsedStack).toEqual({
+ message: 'message',
+ name: 'some name',
+ body: {
+ message: 'a message',
+ status_code: 200,
+ },
+ });
+ });
+ });
+
+ describe('errorToErrorStack', () => {
+ it('works with an Error', async () => {
+ const error: Error = {
+ message: 'message',
+ name: 'some name',
+ };
+ const result = errorToErrorStack(error);
+ expect(result).toEqual(Error('message'));
+ });
+
+ it('creates a stack trace of an Error', async () => {
+ const error: Error = {
+ message: 'message',
+ name: 'some name',
+ };
+ const result = errorToErrorStack(error);
+ const parsedStack = JSON.parse(result.stack ?? '');
+ expect(parsedStack).toEqual({
+ message: 'message',
+ name: 'some name',
+ });
+ });
+ });
+
+ describe('unknownToErrorStack', () => {
+ it('works with a string', async () => {
+ const error = 'error';
+ const result = unknownToErrorStack(error);
+ expect(result).toEqual(Error('error'));
+ });
+
+ it('works with an object that has fields by using a stringification of it', async () => {
+ const error = { a: 1, b: 1 };
+ const result = unknownToErrorStack(error);
+ expect(result).toEqual(Error(JSON.stringify(error, null, 2)));
+ });
+
+ it('works with an an array that has fields by using a stringification of it', async () => {
+ const error = [{ a: 1, b: 1 }];
+ const result = unknownToErrorStack(error);
+ expect(result).toEqual(Error(JSON.stringify(error, null, 2)));
+ });
+
+ it('does create a stack error from a plain string of that string', async () => {
+ const error = 'error';
+ const result = unknownToErrorStack(error);
+ const parsedStack = JSON.parse(result.stack ?? '');
+ expect(parsedStack).toEqual(error);
+ });
+
+ it('does create a stack with an object that has fields by using a stringification of it', async () => {
+ const error = { a: 1, b: 1 };
+ const result = unknownToErrorStack(error);
+ const parsedStack = JSON.parse(result.stack ?? '');
+ expect(parsedStack).toEqual(error);
+ });
+
+ it('does create a stack with an an array that has fields by using a stringification of it', async () => {
+ const error = [{ a: 1, b: 1 }];
+ const result = unknownToErrorStack(error);
+ const parsedStack = JSON.parse(result.stack ?? '');
+ expect(parsedStack).toEqual(error);
+ });
+ });
+
+ describe('getStringifiedStack', () => {
+ it('works with an Error object', async () => {
+ const result = getStringifiedStack(new Error('message'));
+ const parsedResult = JSON.parse(result ?? '');
+ expect(parsedResult.name).toEqual('Error');
+ expect(parsedResult.message).toEqual('message');
+ expect(parsedResult.stack).toEqual(expect.stringContaining('Error: message'));
+ });
+
+ it('works with a regular object', async () => {
+ const regularObject = { a: 'regular object' };
+ const result = getStringifiedStack(regularObject);
+ const parsedResult = JSON.parse(result ?? '');
+ expect(parsedResult).toEqual(regularObject);
+ });
+
+ it('returns undefined with a circular reference', async () => {
+ const circleRef = { a: {} };
+ circleRef.a = circleRef;
+ const result = getStringifiedStack(circleRef);
+ expect(result).toEqual(undefined);
+ });
+
+ it('returns undefined if given an empty object', async () => {
+ const emptyObj = {};
+ const result = getStringifiedStack(emptyObj);
+ expect(result).toEqual(undefined);
+ });
+
+ it('returns a string if given a string', async () => {
+ const stringValue = 'some value';
+ const result = getStringifiedStack(stringValue);
+ expect(result).toEqual(`"${stringValue}"`);
+ });
+
+ it('returns an array if given an array', async () => {
+ const value = ['some value'];
+ const result = getStringifiedStack(value);
+ const parsedResult = JSON.parse(result ?? '');
+ expect(parsedResult).toEqual(value);
+ });
+
+ it('removes top level empty objects if found to clean things up a bit', async () => {
+ const objectWithEmpties = { a: {}, b: { c: 1 }, d: {}, e: {} };
+ const result = getStringifiedStack(objectWithEmpties);
+ const parsedResult = JSON.parse(result ?? '');
+ expect(parsedResult).toEqual({ b: { c: 1 } });
+ });
+ });
+
+ describe('convertErrorToEnumerable', () => {
+ test('it will return a stringable Error object', () => {
+ const converted = convertErrorToEnumerable(new Error('message'));
+ // delete the stack off the converted for testing determinism
+ delete (converted as Error).stack;
+ expect(JSON.stringify(converted)).toEqual(
+ JSON.stringify({ name: 'Error', message: 'message' })
+ );
+ });
+
+ test('it will return a value not touched if it is not an error instances', () => {
+ const obj = { a: 1 };
+ const converted = convertErrorToEnumerable(obj);
+ expect(converted).toBe(obj);
+ });
+ });
+
+ describe('isEmptyObjectWhenStringified', () => {
+ test('it returns false when handed a non-object', () => {
+ expect(isEmptyObjectWhenStringified('string')).toEqual(false);
+ });
+
+ test('it returns false when handed a non-empty object', () => {
+ expect(isEmptyObjectWhenStringified({ a: 1 })).toEqual(false);
+ });
+
+ test('it returns true when handed an empty object', () => {
+ expect(isEmptyObjectWhenStringified({})).toEqual(true);
});
});
});
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
index f5a3c75747e52..61b20e137f870 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
@@ -6,11 +6,12 @@
*/
import { useCallback, useRef } from 'react';
+import { isString } from 'lodash/fp';
import { IEsError, isEsError } from '../../../../../../src/plugins/data/public';
import { ErrorToastOptions, ToastsStart, Toast } from '../../../../../../src/core/public';
import { useToasts } from '../lib/kibana';
-import { isAppError } from '../utils/api';
+import { AppError, isAppError, isKibanaError, isSecurityAppError } from '../utils/api';
export type UseAppToasts = Pick & {
api: ToastsStart;
@@ -32,32 +33,40 @@ export const useAppToasts = (): UseAppToasts => {
const _addError = useCallback(
(error: unknown, options: ErrorToastOptions) => {
- if (error != null && isEsError(error)) {
- const err = esErrorToRequestError(error);
- return addError(err, options);
- } else if (isAppError(error)) {
- return addError(error, options);
- } else if (error instanceof Error) {
- return addError(error, options);
- } else {
- // Best guess that this is a stringable error.
- const err = new Error(String(error));
- return addError(err, options);
- }
+ const adaptedError = errorToErrorStackAdapter(error);
+ return addError(adaptedError, options);
},
[addError]
);
-
return { api: toasts, addError: _addError, addSuccess, addWarning };
};
+/**
+ * Given an error of one type vs. another type this tries to adapt
+ * the best it can to the existing error toaster which parses the .stack
+ * as its error when you click the button to show the full error message.
+ * @param error The error to adapt to.
+ * @returns The adapted toaster error message.
+ */
+export const errorToErrorStackAdapter = (error: unknown): Error => {
+ if (error != null && isEsError(error)) {
+ return esErrorToErrorStack(error);
+ } else if (isAppError(error)) {
+ return appErrorToErrorStack(error);
+ } else if (error instanceof Error) {
+ return errorToErrorStack(error);
+ } else {
+ return unknownToErrorStack(error);
+ }
+};
+
/**
* See this file, we are not allowed to import files such as es_error.
* So instead we say maybe err is on there so that we can unwrap it and get
* our status code from it if possible within the error in our function.
* src/plugins/data/public/search/errors/es_error.tsx
*/
-type MaybeESError = IEsError & { err?: Record };
+export type MaybeESError = IEsError & { err?: Record };
/**
* This attempts its best to map between an IEsError which comes from bsearch to a error_toaster
@@ -72,13 +81,152 @@ type MaybeESError = IEsError & { err?: Record };
*
* Where this same technique of overriding and changing the stack is occurring.
*/
-export const esErrorToRequestError = (error: IEsError & MaybeESError): Error => {
+export const esErrorToErrorStack = (error: IEsError & MaybeESError): Error => {
const maybeUnWrapped = error.err != null ? error.err : error;
- const statusCode = error.err?.statusCode != null ? `(${error.err.statusCode})` : '';
- const stringifiedError = JSON.stringify(maybeUnWrapped, null, 2);
- return {
- message: `${error.attributes?.reason ?? error.message} ${statusCode}`,
- name: error.attributes?.reason ?? error.message,
- stack: stringifiedError,
- };
+ const statusCode =
+ error.err?.statusCode != null
+ ? `(${error.err.statusCode})`
+ : error.statusCode != null
+ ? `(${error.statusCode})`
+ : '';
+ const stringifiedError = getStringifiedStack(maybeUnWrapped);
+ const adaptedError = new Error(`${error.attributes?.reason ?? error.message} ${statusCode}`);
+ adaptedError.name = error.attributes?.reason ?? error.message;
+ if (stringifiedError != null) {
+ adaptedError.stack = stringifiedError;
+ }
+ return adaptedError;
+};
+
+/**
+ * This attempts its best to map between a Kibana application error which can come from backend
+ * REST API's that are typically of a particular format and form.
+ *
+ * The existing error_toaster code tries to consolidate network and software stack traces but really
+ * here and our toasters we are using them for network response errors so we can troubleshoot things
+ * as quick as possible.
+ *
+ * We override and use error.stack to be able to give _full_ network responses regardless of if they
+ * are from Kibana or if they are from elasticSearch since sometimes Kibana errors might wrap the errors.
+ *
+ * Sometimes the errors are wrapped from io-ts, Kibana Schema or something else and we want to show
+ * as full error messages as we can.
+ */
+export const appErrorToErrorStack = (error: AppError): Error => {
+ const statusCode = isKibanaError(error)
+ ? `(${error.body.statusCode})`
+ : isSecurityAppError(error)
+ ? `(${error.body.status_code})`
+ : '';
+ const stringifiedError = getStringifiedStack(error);
+ const adaptedError = new Error(
+ `${String(error.body.message).trim() !== '' ? error.body.message : error.message} ${statusCode}`
+ );
+ // Note although all the Typescript typings say that error.name is a string and exists, we still can encounter an undefined so we
+ // do an extra guard here and default to empty string if it is undefined
+ adaptedError.name = error.name != null ? error.name : '';
+ if (stringifiedError != null) {
+ adaptedError.stack = stringifiedError;
+ }
+ return adaptedError;
+};
+
+/**
+ * Takes an error and tries to stringify it and use that as the stack for the error toaster
+ * @param error The error to convert into a message
+ * @returns The exception error to return back
+ */
+export const errorToErrorStack = (error: Error): Error => {
+ const stringifiedError = getStringifiedStack(error);
+ const adaptedError = new Error(error.message);
+ adaptedError.name = error.name;
+ if (stringifiedError != null) {
+ adaptedError.stack = stringifiedError;
+ }
+ return adaptedError;
+};
+
+/**
+ * Last ditch effort to take something unknown which could be a string, number,
+ * anything. This usually should not be called but just in case we do try our
+ * best to stringify it and give a message, name, and replace the stack of it.
+ * @param error The unknown error to convert into a message
+ * @returns The exception error to return back
+ */
+export const unknownToErrorStack = (error: unknown): Error => {
+ const stringifiedError = getStringifiedStack(error);
+ const message = isString(error)
+ ? error
+ : error instanceof Object && stringifiedError != null
+ ? stringifiedError
+ : String(error);
+ const adaptedError = new Error(message);
+ adaptedError.name = message;
+ if (stringifiedError != null) {
+ adaptedError.stack = stringifiedError;
+ }
+ return adaptedError;
+};
+
+/**
+ * Stringifies the error. However, since Errors can JSON.stringify into empty objects this will
+ * use a replacer to push those as enumerable properties so we can stringify them.
+ * @param error The error to get a string representation of
+ * @returns The string representation of the error
+ */
+export const getStringifiedStack = (error: unknown): string | undefined => {
+ try {
+ return JSON.stringify(
+ error,
+ (_, value) => {
+ const enumerable = convertErrorToEnumerable(value);
+ if (isEmptyObjectWhenStringified(enumerable)) {
+ return undefined;
+ } else {
+ return enumerable;
+ }
+ },
+ 2
+ );
+ } catch (err) {
+ return undefined;
+ }
+};
+
+/**
+ * Converts an error if this is an error to have enumerable so it can stringified
+ * @param error The error which might not have enumerable properties.
+ * @returns Enumerable error
+ */
+export const convertErrorToEnumerable = (error: unknown): unknown => {
+ if (error instanceof Error) {
+ return {
+ ...error,
+ name: error.name,
+ message: error.message,
+ stack: error.stack,
+ };
+ } else {
+ return error;
+ }
+};
+
+/**
+ * If the object strings into an empty object we shouldn't show it as it doesn't
+ * add value and sometimes different people/frameworks attach req,res,request,response
+ * objects which don't stringify into anything or can have circular references.
+ * @param item The item to see if we are empty or have a circular reference error with.
+ * @returns True if this is a good object to stringify, otherwise false
+ */
+export const isEmptyObjectWhenStringified = (item: unknown): boolean => {
+ if (item instanceof Object) {
+ try {
+ return JSON.stringify(item) === '{}';
+ } catch (_) {
+ // Do nothing, return false if we have a circular reference or other oddness.
+ return false;
+ }
+ } else {
+ return false;
+ }
};