From f3cd3a6368f16ee60f90b8cce5e14e3f0660d8f8 Mon Sep 17 00:00:00 2001 From: saseungmin Date: Tue, 20 Jul 2021 04:28:18 +0900 Subject: [PATCH] =?UTF-8?q?[Feature]=20=EC=97=90=EB=9F=AC=20=ED=8A=B8?= =?UTF-8?q?=EB=A0=88=ED=82=B9=EC=9D=84=20=EC=9C=84=ED=95=9C=20sentry=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EA=B5=AC=EA=B8=80=20=EC=96=B4?= =?UTF-8?q?=EB=84=90=EB=A6=AC=ED=8B=B1=EC=8A=A4=EB=A5=BC=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=9D=BC=20#220?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Install Sentry/react & tracing dependencies - Apply Google Analytics - Implements and Apply CrushErrorPage in ErrorBoundary - Change folder location ErrorScreenTemplate --- package-lock.json | 193 ++++++++++++++++++ package.json | 2 + public/index.html | 10 + src/ErrorBoundary.jsx | 23 ++- src/ErrorBoundary.test.jsx | 46 ++++- .../{common => error}/ErrorScreenTemplate.jsx | 0 .../ErrorScreenTemplate.test.jsx | 0 src/containers/error/CrashErrorContainer.jsx | 24 +++ .../error/CrashErrorContainer.test.jsx | 46 +++++ src/containers/error/NotFoundContainer.jsx | 2 +- .../error/NotFoundContainer.test.jsx | 10 +- src/index.jsx | 13 +- src/pages/CrashErrorPage.jsx | 16 ++ src/pages/CrashErrorPage.test.jsx | 39 ++++ src/pages/NotFoundPage.jsx | 2 +- .../{error_test.js => not_found_test.js} | 2 +- 16 files changed, 403 insertions(+), 25 deletions(-) rename src/components/{common => error}/ErrorScreenTemplate.jsx (100%) rename src/components/{common => error}/ErrorScreenTemplate.test.jsx (100%) create mode 100644 src/containers/error/CrashErrorContainer.jsx create mode 100644 src/containers/error/CrashErrorContainer.test.jsx create mode 100644 src/pages/CrashErrorPage.jsx create mode 100644 src/pages/CrashErrorPage.test.jsx rename tests/error/{error_test.js => not_found_test.js} (91%) 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');