Skip to content

Commit

Permalink
feat: ADDON-57381 hide implementation details from user-facing error …
Browse files Browse the repository at this point in the history
…messages (#987)

https://splunk.atlassian.net/browse/ADDON-57381 

- [x] parse BE errors with omitting stack traces
- [x] move error message to the center
- [x] get rid of bindings on error codes on FE side. Just show BE error
- [x] remove AxiosError - it is too technical to the user

---------

Co-authored-by: Viktor Tsvetkov <142901247+vtsvetkov-splunk@users.noreply.github.com>
  • Loading branch information
soleksy-splunk and vtsvetkov-splunk authored Feb 19, 2024
1 parent 0a9103e commit 5467ed9
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 136 deletions.
2 changes: 1 addition & 1 deletion ui/src/components/BaseFormView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ class BaseFormView extends PureComponent<BaseFormProps, BaseFormState> {
if (entityLabel && nameFromDict && typeof nameFromDict !== 'object') {
this.setErrorFieldMsg(
'name',
getFormattedMessage(2, [entityLabel, nameFromDict])
getFormattedMessage(2, [entityLabel, String(nameFromDict)])
);
}
}
Expand Down
4 changes: 1 addition & 3 deletions ui/src/components/ConfigurationFormView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ function ConfigurationFormView({ serviceName }) {
serviceName: `settings/${serviceName}`,
handleError: true,
callbackOnError: (err) => {
// eslint-disable-next-line no-param-reassign
err.uccErrorCode = 'ERR0005';
setError(err);
},
}).then((response) => {
Expand All @@ -48,7 +46,7 @@ function ConfigurationFormView({ serviceName }) {
setIsSubmitting(set);
};

if (error?.uccErrorCode) {
if (error) {
throw error;
}

Expand Down
94 changes: 61 additions & 33 deletions ui/src/components/ErrorBoundary/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,99 @@
import React, { ReactElement } from 'react';
import Heading from '@splunk/react-ui/Heading';
import { _ } from '@splunk/ui-utils/i18n';
import { gettext } from '@splunk/ui-utils/i18n';
import Card from '@splunk/react-ui/Card';
import WarningIcon from '@splunk/react-icons/enterprise/Warning';
import errorCodes from '../../constants/errorCodes';
import styled from 'styled-components';
import { variables } from '@splunk/themes';
import { parseErrorMsg } from '../../util/messageUtil';

interface ErrorBoundaryProps {
children: ReactElement | ReactElement[];
}

interface ErrorBoundaryState {
errorCode: keyof typeof errorCodes | null;
error: null | unknown;
error:
| {
response?: {
data?: {
messages?: { text: string }[];
};
};
}
| null
| unknown;
}

const StyledContainer = styled.div`
display: flex;
justify-content: center; // Ensures horizontal centering of children
align-items: center; // Ensures vertical centering
width: 100%; // Takes up full width of its parent
`;

const StyledCard = styled(Card)`
display: flex;
flex: 0;
box-shadow: ${variables.overlayShadow};
min-width: 30rem;
`;

const StyledHeading = styled(Heading)`
text-align: center;
`;

const StyledWarningIcon = styled(WarningIcon)`
font-size: 120px;
color: ${variables.alertColor};
`;

const StyledTypography = styled.details`
white-space: pre-wrap;
word-break: break-word;
`;

class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { errorCode: null, error: null };
this.state = { error: null };
}

static getDerivedStateFromError(error: { uccErrorCode: unknown }) {
static getDerivedStateFromError(error: unknown) {
// Update state so the next render will show the fallback UI.
return { errorCode: error.uccErrorCode, error };
return { error };
}

componentDidCatch(error: unknown) {
// Catch errors in any components below and re-render with error message
this.setState({
error,
});
// You can also log error messages to an error reporting service here
// eslint-disable-next-line no-console
console.error(error);
}

render() {
if (this.state.error) {
const parsedErrorMessage = parseErrorMsg(this.state?.error);
// Error path
return (
<div style={{ marginTop: '10%' }}>
<Card style={{ boxShadow: '10px 10px 5px #aaaaaa' }}>
<StyledContainer>
<StyledCard>
<Card.Header>
<Heading style={{ textAlign: 'center' }} level={2}>
<WarningIcon style={{ fontSize: '120px', color: '#ff9900' }} />
<br />
<br />
{this.state.errorCode === 'ERR0001'
? _('Failed to load Inputs Page')
: _('Something went wrong!')}
</Heading>
<StyledHeading level={2}>
<StyledWarningIcon />
<StyledTypography as="p">
{gettext('Something went wrong!')}
</StyledTypography>
</StyledHeading>
</Card.Header>
<Card.Body>
{this.state.errorCode ? (
<>
{_(errorCodes[this.state.errorCode])}
<br />
<br />
</>
) : null}
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error?.toString()}
</details>
{parsedErrorMessage && (
<StyledTypography as="p">{parsedErrorMessage}</StyledTypography>
)}
</Card.Body>
<Card.Footer showBorder={false}>
{this.state.errorCode ? this.state.errorCode : null}
</Card.Footer>
</Card>
</div>
</StyledCard>
</StyledContainer>
);
}
// Normally, just render children
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import ErrorBoundary from '../ErrorBoundary';

const ErrorGenerator = () => {
throw new Error('some error message');
throw new Error('Some internal error message. It should not be shown to the user');
};

const meta = {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 8 additions & 23 deletions ui/src/components/table/TableWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import CustomTable from './CustomTable';
import TableHeader from './TableHeader';
import TableContext from '../../context/TableContext';
import { PAGE_INPUT } from '../../constants/pages';
import { parseErrorMsg } from '../../util/messageUtil';

function TableWrapper({
page,
Expand Down Expand Up @@ -76,29 +77,13 @@ function TableWrapper({
});
axios
.all(requests)
// eslint-disable-next-line no-shadow
.catch((error) => {
let message = '';
let errorCode = '';
if (error.response) {
// The request was made and the server responded with a status code
message = `Error received from server: ${error.response.data.messages[0].text}`;
errorCode = page === PAGE_INPUT ? 'ERR0001' : 'ERR0002';
} else if (error.request) {
// The request was made but no response was received
message = `No response received while making request to ${page} services`;
errorCode = 'ERR0003';
} else {
// Something happened in setting up the request that triggered an Error
message = `Error making request to ${page} services`;
errorCode = 'ERR0004';
}
// eslint-disable-next-line no-param-reassign
error.uccErrorCode = errorCode;
generateToast(message);
.catch((caughtError) => {
const message = parseErrorMsg(caughtError);

generateToast(message, 'error');
setLoading(false);
setError(error);
return Promise.reject(error);
setError(caughtError);
return Promise.reject(caughtError);
})
.then((response) => {
modifyAPIResponse(response.map((res) => res.data.entry));
Expand Down Expand Up @@ -245,7 +230,7 @@ function TableWrapper({
return [updatedArr, arr.length, arr];
};

if (error?.uccErrorCode) {
if (error) {
throw error;
}

Expand Down
16 changes: 0 additions & 16 deletions ui/src/constants/errorCodes.tsx

This file was deleted.

6 changes: 4 additions & 2 deletions ui/src/constants/messageDict.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default {
const MESSAGE_DICT: Record<number | 'unknown', string> = {
/* validation messages, range [0, 99] */
0: 'Field {{args[0]}} is required',
1: 'Field {{args[0]}} must be a string',
Expand Down Expand Up @@ -59,4 +59,6 @@ export default {
118: 'configuration file not found',

unknown: 'An unknown error occurred',
} as const;
};

export default MESSAGE_DICT;
14 changes: 3 additions & 11 deletions ui/src/util/axiosCallWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import axios, { CancelToken } from 'axios';
import { CSRFToken, app } from '@splunk/splunk-utils/config';
import { createRESTURL } from '@splunk/splunk-utils/url';
import { generateEndPointUrl, generateToast } from './util';
import { parseErrorMsg } from './messageUtil';

interface axiosCallWithServiceName {
serviceName?: string;
Expand Down Expand Up @@ -82,20 +83,11 @@ const axiosCallWrapper = ({

return handleError
? axios(options).catch((error) => {
let message = '';
if (axios.isCancel(error)) {
return Promise.reject(error);
}
if (error.response) {
// The request was made and the server responded with a status code
message = `Error response received from server: ${error.response.data.messages[0].text}`;
} else if (error.request) {
// The request was made but no response was received
message = `No response received while making request to ${endpoint}`;
} else {
// Something happened in setting up the request that triggered an Error
message = `Error making ${method} request to ${endpoint}`;
}
const message = parseErrorMsg(error);

generateToast(message, 'error');
callbackOnError(error);
return Promise.reject(error);
Expand Down
44 changes: 0 additions & 44 deletions ui/src/util/messageUtil.js

This file was deleted.

64 changes: 64 additions & 0 deletions ui/src/util/messageUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as __ from 'lodash';
import { _ } from '@splunk/ui-utils/i18n';
import messageDict from '../constants/messageDict';

/**
* @param code a int value.
* @param msg arguments to format the message.
*/
export const getFormattedMessage = (code: number, msg?: (string | number | boolean)[]): string => {
let template = messageDict[code] || messageDict.unknown;
template = _(template);
return __.template(template, {
escape: /\{\{(.+?)\}\}/g,
})({
args: msg,
});
};

const tryParseByteString = (text: string) => {
try {
if (text.startsWith(`b'`) || text.startsWith(`b"`)) {
// bytestring starts from b and Quotation mark (b') and ends with Quotation mark(')
const parsedString = JSON.parse(text.slice(2, -1));
return String(parsedString.messages[0].text);
}
return text;
} catch {
return text;
}
};

export const tryTrimErrorMessage = (msg: string) => {
try {
const regex =
/.+"REST Error \[[\d]+\]:\s+.+\s+--\s+([\s\S]*)"\.\s*See splunkd\.log(\/python.log)? for more details\./;
const matches = regex.exec(msg);
if (matches && matches[1]) {
try {
const innerMsgJSON = JSON.parse(matches[1]);
return String(innerMsgJSON.messages[0].text);
} catch (error) {
return tryParseByteString(matches[1]);
}
}
} catch (e) {
return msg;
}

return msg;
};

export const parseErrorMsg = (err?: {
response?: { data?: { messages?: { text?: string }[] } };
}) => {
try {
const msg = err?.response?.data?.messages?.[0]?.text;
if (!msg) {
return messageDict.unknown;
}
return tryTrimErrorMessage(msg);
} catch (e) {
return _('Error in processing the request');
}
};

0 comments on commit 5467ed9

Please sign in to comment.