diff --git a/package-lock.json b/package-lock.json
index f524013..1b1ebfa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,8 @@
"@emotion/react": "^11.0.0",
"@emotion/styled": "^11.0.0",
"@reduxjs/toolkit": "^1.4.0",
+ "@sentry/react": "^6.9.0",
+ "@sentry/tracing": "^6.9.0",
"d2coding": "^1.3.2",
"draft-js": "^0.11.7",
"draftjs-to-html": "^0.9.1",
@@ -3305,6 +3307,115 @@
"reselect": "^4.0.0"
}
},
+ "node_modules/@sentry/browser": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.9.0.tgz",
+ "integrity": "sha512-4JnEPcwoNs6JqeEd4wscBq+hxpotEJ0DJ4eOIsaNZIMyqEHXBHTXCk/gfrSsiZFrkHM4PgvUHOxaC0HcZ92oBA==",
+ "dependencies": {
+ "@sentry/core": "6.9.0",
+ "@sentry/types": "6.9.0",
+ "@sentry/utils": "6.9.0",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@sentry/core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.9.0.tgz",
+ "integrity": "sha512-oFX2qQcMLujCeIuCQGlhpTUIOXiU5n6V2lqDnvMXUV8gKpplBPalwdlR9bgbSi+VO8u7LjHR1IKM0RAPWgNHWw==",
+ "dependencies": {
+ "@sentry/hub": "6.9.0",
+ "@sentry/minimal": "6.9.0",
+ "@sentry/types": "6.9.0",
+ "@sentry/utils": "6.9.0",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@sentry/hub": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.9.0.tgz",
+ "integrity": "sha512-5mors7ojbo7G85ZmoVPQBgFBMONAJwyZfV0LNLy14GenoaVNuxTPyvAQiJb1FYq+x6YZ3CvqGX6r74KRKQU87w==",
+ "dependencies": {
+ "@sentry/types": "6.9.0",
+ "@sentry/utils": "6.9.0",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@sentry/minimal": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.9.0.tgz",
+ "integrity": "sha512-GBZ6wG2Rc1wInYEl2BZTZc/t57O1Da876ifLsSPpEQAEnGWbqZWb8RLjZskH09ZIL/K4XCIDDi5ySzN8kFUWJw==",
+ "dependencies": {
+ "@sentry/hub": "6.9.0",
+ "@sentry/types": "6.9.0",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@sentry/react": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.9.0.tgz",
+ "integrity": "sha512-ccMhpL+YHcq171EhSHU02IYh476mBjPfK1zq+vW2fJkaigg+mEqbOHnQV0Uu3zFYHGqVg4CZKZc6v92cvbBwEg==",
+ "dependencies": {
+ "@sentry/browser": "6.9.0",
+ "@sentry/minimal": "6.9.0",
+ "@sentry/types": "6.9.0",
+ "@sentry/utils": "6.9.0",
+ "hoist-non-react-statics": "^3.3.2",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "peerDependencies": {
+ "react": "15.x || 16.x || 17.x"
+ }
+ },
+ "node_modules/@sentry/tracing": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.9.0.tgz",
+ "integrity": "sha512-gogVTypolhPazXr3Lue8HgzBg5Sy1cQpEp5Iq9LtECs+TlOlxJ+S+P+EIjEZ0f1AHVu706jr5cY2G2Shluli9g==",
+ "dependencies": {
+ "@sentry/hub": "6.9.0",
+ "@sentry/minimal": "6.9.0",
+ "@sentry/types": "6.9.0",
+ "@sentry/utils": "6.9.0",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@sentry/types": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.9.0.tgz",
+ "integrity": "sha512-v52HJqLoLapEnqS2NdVtUXPvT+aezQgNXQkp8hiQ3RUdTm5cffwBVG7wlbpE6OsOOIZxd6p1zKylFkwCypiIIA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@sentry/utils": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.9.0.tgz",
+ "integrity": "sha512-PimDr6KAi4cCp5hQZ8Az2/pDcdfhTu7WAU30Dd9MZwknpHSTmD4G6QvkdrB5er6kMMnNQOC7rMo6w/Do3m6X3w==",
+ "dependencies": {
+ "@sentry/types": "6.9.0",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@sideway/address": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.1.tgz",
@@ -27419,6 +27530,88 @@
"reselect": "^4.0.0"
}
},
+ "@sentry/browser": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.9.0.tgz",
+ "integrity": "sha512-4JnEPcwoNs6JqeEd4wscBq+hxpotEJ0DJ4eOIsaNZIMyqEHXBHTXCk/gfrSsiZFrkHM4PgvUHOxaC0HcZ92oBA==",
+ "requires": {
+ "@sentry/core": "6.9.0",
+ "@sentry/types": "6.9.0",
+ "@sentry/utils": "6.9.0",
+ "tslib": "^1.9.3"
+ }
+ },
+ "@sentry/core": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.9.0.tgz",
+ "integrity": "sha512-oFX2qQcMLujCeIuCQGlhpTUIOXiU5n6V2lqDnvMXUV8gKpplBPalwdlR9bgbSi+VO8u7LjHR1IKM0RAPWgNHWw==",
+ "requires": {
+ "@sentry/hub": "6.9.0",
+ "@sentry/minimal": "6.9.0",
+ "@sentry/types": "6.9.0",
+ "@sentry/utils": "6.9.0",
+ "tslib": "^1.9.3"
+ }
+ },
+ "@sentry/hub": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.9.0.tgz",
+ "integrity": "sha512-5mors7ojbo7G85ZmoVPQBgFBMONAJwyZfV0LNLy14GenoaVNuxTPyvAQiJb1FYq+x6YZ3CvqGX6r74KRKQU87w==",
+ "requires": {
+ "@sentry/types": "6.9.0",
+ "@sentry/utils": "6.9.0",
+ "tslib": "^1.9.3"
+ }
+ },
+ "@sentry/minimal": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.9.0.tgz",
+ "integrity": "sha512-GBZ6wG2Rc1wInYEl2BZTZc/t57O1Da876ifLsSPpEQAEnGWbqZWb8RLjZskH09ZIL/K4XCIDDi5ySzN8kFUWJw==",
+ "requires": {
+ "@sentry/hub": "6.9.0",
+ "@sentry/types": "6.9.0",
+ "tslib": "^1.9.3"
+ }
+ },
+ "@sentry/react": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.9.0.tgz",
+ "integrity": "sha512-ccMhpL+YHcq171EhSHU02IYh476mBjPfK1zq+vW2fJkaigg+mEqbOHnQV0Uu3zFYHGqVg4CZKZc6v92cvbBwEg==",
+ "requires": {
+ "@sentry/browser": "6.9.0",
+ "@sentry/minimal": "6.9.0",
+ "@sentry/types": "6.9.0",
+ "@sentry/utils": "6.9.0",
+ "hoist-non-react-statics": "^3.3.2",
+ "tslib": "^1.9.3"
+ }
+ },
+ "@sentry/tracing": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.9.0.tgz",
+ "integrity": "sha512-gogVTypolhPazXr3Lue8HgzBg5Sy1cQpEp5Iq9LtECs+TlOlxJ+S+P+EIjEZ0f1AHVu706jr5cY2G2Shluli9g==",
+ "requires": {
+ "@sentry/hub": "6.9.0",
+ "@sentry/minimal": "6.9.0",
+ "@sentry/types": "6.9.0",
+ "@sentry/utils": "6.9.0",
+ "tslib": "^1.9.3"
+ }
+ },
+ "@sentry/types": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.9.0.tgz",
+ "integrity": "sha512-v52HJqLoLapEnqS2NdVtUXPvT+aezQgNXQkp8hiQ3RUdTm5cffwBVG7wlbpE6OsOOIZxd6p1zKylFkwCypiIIA=="
+ },
+ "@sentry/utils": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.9.0.tgz",
+ "integrity": "sha512-PimDr6KAi4cCp5hQZ8Az2/pDcdfhTu7WAU30Dd9MZwknpHSTmD4G6QvkdrB5er6kMMnNQOC7rMo6w/Do3m6X3w==",
+ "requires": {
+ "@sentry/types": "6.9.0",
+ "tslib": "^1.9.3"
+ }
+ },
"@sideway/address": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.1.tgz",
diff --git a/package.json b/package.json
index bbd0768..67a51b4 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,8 @@
"@emotion/react": "^11.0.0",
"@emotion/styled": "^11.0.0",
"@reduxjs/toolkit": "^1.4.0",
+ "@sentry/react": "^6.9.0",
+ "@sentry/tracing": "^6.9.0",
"d2coding": "^1.3.2",
"draft-js": "^0.11.7",
"draftjs-to-html": "^0.9.1",
diff --git a/public/index.html b/public/index.html
index 3785bcc..46e41d2 100644
--- a/public/index.html
+++ b/public/index.html
@@ -5,6 +5,16 @@
+
+
+
+
diff --git a/src/ErrorBoundary.jsx b/src/ErrorBoundary.jsx
index 3b34fb1..1efadd4 100644
--- a/src/ErrorBoundary.jsx
+++ b/src/ErrorBoundary.jsx
@@ -2,9 +2,12 @@
/* eslint-disable react/destructuring-assignment */
import React from 'react';
+import * as Sentry from '@sentry/react';
+
import useNotFound from './hooks/useNotFound';
import NotFoundPage from './pages/NotFoundPage';
+import CrashErrorPage from './pages/CrashErrorPage';
function ErrorBoundaryWrapper({ children }) {
const { isNotFound } = useNotFound();
@@ -19,21 +22,31 @@ function ErrorBoundaryWrapper({ children }) {
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
- this.state = { hasError: false };
+ this.state = {
+ hasError: false,
+ };
+ this.handleResolveError = this.handleResolveError.bind(this);
}
static getDerivedStateFromError(error) {
- return { hasError: true };
+ return {
+ hasError: true,
+ };
}
componentDidCatch(error, errorInfo) {
- // TODO - 추후 로직 추가
+ Sentry.captureException(error);
+ }
+
+ handleResolveError() {
+ this.setState({
+ hasError: false,
+ });
}
render() {
if (this.state.hasError) {
- // TODO - 알 수 없는 오류 페이지 추가
- return
앗! 알 수 없는 오류가 발생했어요!
;
+ return ;
}
return (
diff --git a/src/ErrorBoundary.test.jsx b/src/ErrorBoundary.test.jsx
index 8c35cba..f51ff28 100644
--- a/src/ErrorBoundary.test.jsx
+++ b/src/ErrorBoundary.test.jsx
@@ -2,11 +2,21 @@ import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
-import { render } from '@testing-library/react';
+import { render, fireEvent } from '@testing-library/react';
import ErrorBoundary from './ErrorBoundary';
import InjectMockProviders from './components/common/test/InjectMockProviders';
+const mockPush = jest.fn();
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useHistory() {
+ return {
+ push: mockPush,
+ };
+ },
+}));
jest.mock('react-redux');
describe('ErrorBoundary', () => {
@@ -26,25 +36,45 @@ describe('ErrorBoundary', () => {
const renderErrorBoundary = (ui) => render(ui);
- context('Has Unknown Error', () => {
+ context('Has Crash Error', () => {
given('errorType', () => null);
const MockComponent = () => {
throw new Error('error');
};
- it('should be renders "앗! 알 수 없는 오류가 발생했어요!" Error Message', () => {
+ it('should be renders "이런.. 오류가 발생했어요!" Error Message', () => {
const { container } = renderErrorBoundary((
-
-
-
+
+
+
+
+
));
- expect(container).toHaveTextContent('앗! 알 수 없는 오류가 발생했어요!');
+ expect(container).toHaveTextContent('이런.. 오류가 발생했어요!');
+ });
+
+ describe('When Crash Error Page, click the "홈으로" button.', () => {
+ it('should be call history push: path is "/"', () => {
+ const { container, getByText } = renderErrorBoundary((
+
+
+
+
+
+ ));
+
+ expect(container).toHaveTextContent('이런.. 오류가 발생했어요!');
+
+ fireEvent.click(getByText('홈으로'));
+
+ expect(mockPush).toBeCalledWith('/');
+ });
});
});
- context("Hasn't Unknown Error", () => {
+ context("Hasn't Crash Error", () => {
context('Without Not Found Error Type', () => {
given('errorType', () => null);
const MockComponent = () => (
diff --git a/src/components/common/ErrorScreenTemplate.jsx b/src/components/error/ErrorScreenTemplate.jsx
similarity index 100%
rename from src/components/common/ErrorScreenTemplate.jsx
rename to src/components/error/ErrorScreenTemplate.jsx
diff --git a/src/components/common/ErrorScreenTemplate.test.jsx b/src/components/error/ErrorScreenTemplate.test.jsx
similarity index 100%
rename from src/components/common/ErrorScreenTemplate.test.jsx
rename to src/components/error/ErrorScreenTemplate.test.jsx
diff --git a/src/containers/error/CrashErrorContainer.jsx b/src/containers/error/CrashErrorContainer.jsx
new file mode 100644
index 0000000..62b19c1
--- /dev/null
+++ b/src/containers/error/CrashErrorContainer.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+import { useHistory } from 'react-router-dom';
+
+import ErrorScreenTemplate from '../../components/error/ErrorScreenTemplate';
+
+const CrashErrorContainer = ({ onResolve }) => {
+ const history = useHistory();
+
+ const onClick = () => {
+ history.push('/');
+ onResolve();
+ };
+
+ return (
+
+ );
+};
+
+export default CrashErrorContainer;
diff --git a/src/containers/error/CrashErrorContainer.test.jsx b/src/containers/error/CrashErrorContainer.test.jsx
new file mode 100644
index 0000000..1693d5f
--- /dev/null
+++ b/src/containers/error/CrashErrorContainer.test.jsx
@@ -0,0 +1,46 @@
+import React from 'react';
+
+import { render, fireEvent } from '@testing-library/react';
+
+import CrashErrorContainer from './CrashErrorContainer';
+
+const mockPush = jest.fn();
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useHistory() {
+ return {
+ push: mockPush,
+ };
+ },
+}));
+
+describe('CrashErrorContainer', () => {
+ const handleResolve = jest.fn();
+
+ beforeEach(() => {
+ handleResolve.mockClear();
+ });
+
+ const renderCrashErrorContainer = () => render((
+
+ ));
+
+ it('should be renders error template contents', () => {
+ const { container } = renderCrashErrorContainer();
+
+ expect(container).toHaveTextContent('이런.. 오류가 발생했어요!');
+ expect(container).toHaveTextContent('홈으로');
+ });
+
+ it('handle Click event', () => {
+ const { getByText } = renderCrashErrorContainer();
+
+ fireEvent.click(getByText('홈으로'));
+
+ expect(mockPush).toBeCalledWith('/');
+ expect(handleResolve).toBeCalled();
+ });
+});
diff --git a/src/containers/error/NotFoundContainer.jsx b/src/containers/error/NotFoundContainer.jsx
index 1cc7974..06e252d 100644
--- a/src/containers/error/NotFoundContainer.jsx
+++ b/src/containers/error/NotFoundContainer.jsx
@@ -9,7 +9,7 @@ import mq from '../../styles/responsive';
import useNotFound from '../../hooks/useNotFound';
import NotFoundSvg from '../../assets/icons/404.svg';
-import ErrorScreenTemplate from '../../components/common/ErrorScreenTemplate';
+import ErrorScreenTemplate from '../../components/error/ErrorScreenTemplate';
const NotFoundImage = styled(NotFoundSvg)`
${mq({
diff --git a/src/containers/error/NotFoundContainer.test.jsx b/src/containers/error/NotFoundContainer.test.jsx
index 77b5a6e..99570a9 100644
--- a/src/containers/error/NotFoundContainer.test.jsx
+++ b/src/containers/error/NotFoundContainer.test.jsx
@@ -31,20 +31,14 @@ describe('NotFoundContainer', () => {
));
it('should be renders error template contents', () => {
- const { container } = renderNotFoundContainer({
- message: '아무것도 없어요!',
- buttonText: '홈으로',
- });
+ const { container } = renderNotFoundContainer();
expect(container).toHaveTextContent('아무것도 없어요!');
expect(container).toHaveTextContent('홈으로');
});
it('handle Click event', () => {
- const { getByText } = renderNotFoundContainer({
- message: '아무것도 없어요!',
- buttonText: '홈으로',
- });
+ const { getByText } = renderNotFoundContainer();
fireEvent.click(getByText('홈으로'));
diff --git a/src/index.jsx b/src/index.jsx
index 831844b..3413b71 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -6,9 +6,20 @@ import { BrowserRouter } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
-import App from './App';
+import * as Sentry from '@sentry/react';
+import { Integrations } from '@sentry/tracing';
import store from './reducers/store';
+import { isDevLevel } from './util/utils';
+
+import App from './App';
+
+Sentry.init({
+ dsn: !isDevLevel(process.env.NODE_ENV) && process.env.SENTRY_DSN,
+ integrations: [new Integrations.BrowserTracing()],
+ environment: process.env.NODE_ENV,
+ tracesSampleRate: 1.0,
+});
ReactDOM.render(
(
diff --git a/src/pages/CrashErrorPage.jsx b/src/pages/CrashErrorPage.jsx
new file mode 100644
index 0000000..91ec872
--- /dev/null
+++ b/src/pages/CrashErrorPage.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { Helmet } from 'react-helmet-async';
+
+import CrashErrorContainer from '../containers/error/CrashErrorContainer';
+
+const CrashErrorPage = ({ onResolve }) => (
+ <>
+
+ 이런.. 오류가 발생했어요!
+
+
+ >
+);
+
+export default CrashErrorPage;
diff --git a/src/pages/CrashErrorPage.test.jsx b/src/pages/CrashErrorPage.test.jsx
new file mode 100644
index 0000000..20086b3
--- /dev/null
+++ b/src/pages/CrashErrorPage.test.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+
+import { render, fireEvent } from '@testing-library/react';
+
+import CrashErrorPage from './CrashErrorPage';
+import InjectMockProviders from '../components/common/test/InjectMockProviders';
+
+describe('CrashErrorPage', () => {
+ const handleResolve = jest.fn();
+
+ beforeEach(() => {
+ handleResolve.mockClear();
+ });
+
+ const renderCrashErrorPage = () => render((
+
+
+
+ ));
+
+ describe('Renders CrashError Contents', () => {
+ it('should be renders Message Text and "홈으로" Button', () => {
+ const { container } = renderCrashErrorPage();
+
+ expect(container).toHaveTextContent('이런.. 오류가 발생했어요!');
+ expect(container).toHaveTextContent('홈으로');
+ });
+
+ it('Click "홈으로" button calls resolve event', () => {
+ const { getByText } = renderCrashErrorPage();
+
+ fireEvent.click(getByText('홈으로'));
+
+ expect(handleResolve).toBeCalled();
+ });
+ });
+});
diff --git a/src/pages/NotFoundPage.jsx b/src/pages/NotFoundPage.jsx
index df087b1..34d46ce 100644
--- a/src/pages/NotFoundPage.jsx
+++ b/src/pages/NotFoundPage.jsx
@@ -7,7 +7,7 @@ import NotFoundContainer from '../containers/error/NotFoundContainer';
const NotFoundPage = () => (
<>
- ConStu - 404
+ 404
>
diff --git a/tests/error/error_test.js b/tests/error/not_found_test.js
similarity index 91%
rename from tests/error/error_test.js
rename to tests/error/not_found_test.js
index 961fb41..c7c19d5 100644
--- a/tests/error/error_test.js
+++ b/tests/error/not_found_test.js
@@ -1,4 +1,4 @@
-Feature('사용자는 에러 상태에 대해서 경험할 수 있다.');
+Feature('사용자는 존재하지 않는 페이지에 접근할 수 있다.');
const step = codeceptjs.container.plugins('commentStep');