diff --git a/package.json b/package.json index d43e799f..c0fffa38 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,13 @@ "build-storybook": "storybook build" }, "dependencies": { + "@sentry/react": "^7.114.0", "@storybook/addon-styling-webpack": "^0.0.5", "@storybook/addon-themes": "^7.6.6", "@tanstack/react-query": "^5.24.7", "@tanstack/react-query-devtools": "^5.24.7", "@testing-library/dom": "^9.3.1", "@types/styled-components": "^5.1.26", - "@vercel/analytics": "^1.1.1", "axios": "^1.4.0", "chart.js": "^4.4.1", "chartjs-plugin-datalabels": "^2.2.0", @@ -39,6 +39,7 @@ "react-helmet-async": "^1.3.0", "react-hooks-testing-library": "^0.6.0", "react-hot-toast": "^2.4.1", + "react-hotjar": "^6.3.1", "react-i18next": "^13.5.0", "react-router-dom": "^6.11.2", "recoil": "^0.7.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ebb3023..1191b870 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@sentry/react': + specifier: ^7.114.0 + version: 7.114.0(react@18.2.0) '@storybook/addon-styling-webpack': specifier: ^0.0.5 version: 0.0.5(webpack@5.90.3) @@ -23,9 +26,6 @@ dependencies: '@types/styled-components': specifier: ^5.1.26 version: 5.1.34 - '@vercel/analytics': - specifier: ^1.1.1 - version: 1.2.2(react@18.2.0) axios: specifier: ^1.4.0 version: 1.6.7 @@ -83,6 +83,9 @@ dependencies: react-hot-toast: specifier: ^2.4.1 version: 2.4.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0) + react-hotjar: + specifier: ^6.3.1 + version: 6.3.1 react-i18next: specifier: ^13.5.0 version: 13.5.0(i18next@23.10.0)(react-dom@18.2.0)(react@18.2.0) @@ -2855,6 +2858,102 @@ packages: picomatch: 2.3.1 dev: true + /@sentry-internal/feedback@7.114.0: + resolution: {integrity: sha512-kUiLRUDZuh10QE9JbSVVLgqxFoD9eDPOzT0MmzlPuas8JlTmJuV4FtSANNcqctd5mBuLt2ebNXH0MhRMwyae4A==} + engines: {node: '>=12'} + dependencies: + '@sentry/core': 7.114.0 + '@sentry/types': 7.114.0 + '@sentry/utils': 7.114.0 + dev: false + + /@sentry-internal/replay-canvas@7.114.0: + resolution: {integrity: sha512-6rTiqmKi/FYtesdM2TM2U+rh6BytdPjLP65KTUodtxohJ+r/3m+termj2o4BhIYPE1YYOZNmbZfwebkuQPmWeg==} + engines: {node: '>=12'} + dependencies: + '@sentry/core': 7.114.0 + '@sentry/replay': 7.114.0 + '@sentry/types': 7.114.0 + '@sentry/utils': 7.114.0 + dev: false + + /@sentry-internal/tracing@7.114.0: + resolution: {integrity: sha512-dOuvfJN7G+3YqLlUY4HIjyWHaRP8vbOgF+OsE5w2l7ZEn1rMAaUbPntAR8AF9GBA6j2zWNoSo8e7GjbJxVofSg==} + engines: {node: '>=8'} + dependencies: + '@sentry/core': 7.114.0 + '@sentry/types': 7.114.0 + '@sentry/utils': 7.114.0 + dev: false + + /@sentry/browser@7.114.0: + resolution: {integrity: sha512-ijJ0vOEY6U9JJADVYGkUbLrAbpGSQgA4zV+KW3tcsBLX9M1jaWq4BV1PWHdzDPPDhy4OgfOjIfaMb5BSPn1U+g==} + engines: {node: '>=8'} + dependencies: + '@sentry-internal/feedback': 7.114.0 + '@sentry-internal/replay-canvas': 7.114.0 + '@sentry-internal/tracing': 7.114.0 + '@sentry/core': 7.114.0 + '@sentry/integrations': 7.114.0 + '@sentry/replay': 7.114.0 + '@sentry/types': 7.114.0 + '@sentry/utils': 7.114.0 + dev: false + + /@sentry/core@7.114.0: + resolution: {integrity: sha512-YnanVlmulkjgZiVZ9BfY9k6I082n+C+LbZo52MTvx3FY6RE5iyiPMpaOh67oXEZRWcYQEGm+bKruRxLVP6RlbA==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.114.0 + '@sentry/utils': 7.114.0 + dev: false + + /@sentry/integrations@7.114.0: + resolution: {integrity: sha512-BJIBWXGKeIH0ifd7goxOS29fBA8BkEgVVCahs6xIOXBjX1IRS6PmX0zYx/GP23nQTfhJiubv2XPzoYOlZZmDxg==} + engines: {node: '>=8'} + dependencies: + '@sentry/core': 7.114.0 + '@sentry/types': 7.114.0 + '@sentry/utils': 7.114.0 + localforage: 1.10.0 + dev: false + + /@sentry/react@7.114.0(react@18.2.0): + resolution: {integrity: sha512-zVPtvSy00Al25Z21f5GNzo3rd/TKS+iOX9wQwLrUZAxyf9RwBxKATLVJNJPkf8dQml6Qx+lfr0BHIlVcr1a1SQ==} + engines: {node: '>=8'} + peerDependencies: + react: 15.x || 16.x || 17.x || 18.x + dependencies: + '@sentry/browser': 7.114.0 + '@sentry/core': 7.114.0 + '@sentry/types': 7.114.0 + '@sentry/utils': 7.114.0 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + dev: false + + /@sentry/replay@7.114.0: + resolution: {integrity: sha512-UvEajoLIX9n2poeW3R4Ybz7D0FgCGXoFr/x/33rdUEMIdTypknxjJWxg6fJngIduzwrlrvWpvP8QiZXczYQy2Q==} + engines: {node: '>=12'} + dependencies: + '@sentry-internal/tracing': 7.114.0 + '@sentry/core': 7.114.0 + '@sentry/types': 7.114.0 + '@sentry/utils': 7.114.0 + dev: false + + /@sentry/types@7.114.0: + resolution: {integrity: sha512-tsqkkyL3eJtptmPtT0m9W/bPLkU7ILY7nvwpi1hahA5jrM7ppoU0IMaQWAgTD+U3rzFH40IdXNBFb8Gnqcva4w==} + engines: {node: '>=8'} + dev: false + + /@sentry/utils@7.114.0: + resolution: {integrity: sha512-319N90McVpupQ6vws4+tfCy/03AdtsU0MurIE4+W5cubHME08HtiEWlfacvAxX+yuKFhvdsO4K4BB/dj54ideg==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.114.0 + dev: false + /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true @@ -4418,21 +4517,6 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vercel/analytics@1.2.2(react@18.2.0): - resolution: {integrity: sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A==} - peerDependencies: - next: '>= 13' - react: ^18 || ^19 - peerDependenciesMeta: - next: - optional: true - react: - optional: true - dependencies: - react: 18.2.0 - server-only: 0.0.1 - dev: false - /@vitejs/plugin-react@3.1.0(vite@4.5.2): resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} engines: {node: ^14.18.0 || >=16.0.0} @@ -7421,6 +7505,10 @@ packages: engines: {node: '>= 4'} dev: true + /immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + dev: false + /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -8508,6 +8596,12 @@ packages: type-check: 0.4.0 dev: true + /lie@3.1.1: + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + dependencies: + immediate: 3.0.6 + dev: false + /lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -8526,6 +8620,12 @@ packages: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} + /localforage@1.10.0: + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + dependencies: + lie: 3.1.1 + dev: false + /locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -9796,6 +9896,10 @@ packages: - csstype dev: false + /react-hotjar@6.3.1: + resolution: {integrity: sha512-EwMqL+ROSlKzatMhT/aqRq7XWWfzlnHynSBSTJh5M2O78mBiPohiSl4Ysls3HOQkkD9y6L22BW0c9bxK2JguwQ==} + dev: false + /react-i18next@13.5.0(i18next@23.10.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA==} peerDependencies: @@ -10350,10 +10454,6 @@ packages: - supports-color dev: true - /server-only@0.0.1: - resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} - dev: false - /set-cookie-parser@2.6.0: resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} dev: false diff --git a/src/App.tsx b/src/App.tsx index f1e470fc..a71be679 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,6 @@ import { lazy, Suspense, useEffect } from 'react'; import { Route, Routes } from 'react-router-dom'; -import { inject } from '@vercel/analytics'; import { useResetRecoilState } from 'recoil'; import ErrorBoundary from '@components/base/ErrorBoundary'; @@ -16,8 +15,6 @@ import { LOCAL_STORAGE_KEY } from '@constants/localStorage.constant'; import PATH from '@constants/path.constant'; import useLocalStorage from '@hooks/useLocalStorage'; -inject(); - const MainPage = lazy(() => import('@pages/main/MainPage')); const SnackGamePage = lazy( diff --git a/src/components/Level/Level.tsx b/src/components/Level/Level.tsx index 3f3e6357..4c6c6590 100644 --- a/src/components/Level/Level.tsx +++ b/src/components/Level/Level.tsx @@ -1,4 +1,4 @@ -import { TIER_COLOR } from '@constants/tier.constant'; +import { TIER_COLOR } from '@constants/common.constant'; interface LevelProps { level: number; diff --git a/src/components/base/ErrorBoundary.tsx b/src/components/base/ErrorBoundary.tsx index bec64d60..c79e9f56 100644 --- a/src/components/base/ErrorBoundary.tsx +++ b/src/components/base/ErrorBoundary.tsx @@ -1,5 +1,7 @@ import { Component, ComponentType, PropsWithChildren } from 'react'; +import * as Sentry from '@sentry/react'; + export interface FallbackProps { error?: Error; resetErrorBoundary?: () => void; @@ -37,6 +39,19 @@ class ErrorBoundary extends Component< return { error }; } + componentDidCatch(error: Error): void { + Sentry.withScope((scope) => { + scope.setLevel('error'); + Sentry.captureMessage( + `[🚨 ${import.meta.env.VITE_NODE_ENV}에러 ${error.name}]: ${window.location.href}`, + ); + + Sentry.captureException(error, { + mechanism: { handled: !!this.props.fallback }, + }); + }); + } + render() { const { fallback: FallbackComponent } = this.props; diff --git a/src/constants/tier.constant.ts b/src/constants/common.constant.ts similarity index 78% rename from src/constants/tier.constant.ts rename to src/constants/common.constant.ts index b9d1255d..47bed2cb 100644 --- a/src/constants/tier.constant.ts +++ b/src/constants/common.constant.ts @@ -1,3 +1,5 @@ +export const TOAST_ID = 'toast'; + export const PRIMARY_COLOR = 'rgba(255, 237, 213, 1)'; export const TIER_COLOR = [ @@ -10,3 +12,6 @@ export const TIER_COLOR = [ 'rgba(223, 164, 225, 1)', 'rgba(239, 191, 191, 1)', ]; + +export const HJID = 4963723; +export const HJSV = 6; diff --git a/src/constants/toast.constant.ts b/src/constants/toast.constant.ts deleted file mode 100644 index 656a1c68..00000000 --- a/src/constants/toast.constant.ts +++ /dev/null @@ -1 +0,0 @@ -export const TOAST_ID = 'toast'; diff --git a/src/hooks/useToast.ts b/src/hooks/useToast.ts index 2549bade..387a42d7 100644 --- a/src/hooks/useToast.ts +++ b/src/hooks/useToast.ts @@ -3,7 +3,7 @@ import { useSetRecoilState } from 'recoil'; import { toastState } from '@utils/atoms/common.atom'; import { ToastType } from '@utils/types/common.type'; -import { TOAST_ID } from '@constants/toast.constant'; +import { TOAST_ID } from '@constants/common.constant'; const useToast = () => { const setToastState = useSetRecoilState(toastState); diff --git a/src/main.tsx b/src/main.tsx index b357efc2..5f23ce46 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,12 +1,16 @@ -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { createRoot } from 'react-dom/client'; import ReactGA from 'react-ga4'; import { HelmetProvider } from 'react-helmet-async'; +import { hotjar } from 'react-hotjar'; import { BrowserRouter } from 'react-router-dom'; +import * as Sentry from '@sentry/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { RecoilRoot } from 'recoil'; +import { HJID, HJSV } from '@constants/common.constant'; + import App from './App'; const queryClient = new QueryClient({ @@ -25,8 +29,30 @@ const queryClient = new QueryClient({ const Root = () => { useEffect(() => { - if (!import.meta.env.VITE_GA_TRACKING_ID) return; - ReactGA.initialize(import.meta.env.VITE_GA_TRACKING_ID); + if (import.meta.env.VITE_NODE_ENV === 'production') { + ReactGA.initialize(import.meta.env.VITE_GA_TRACKING_ID); + + hotjar.initialize({ id: HJID, sv: HJSV }); + } + + if (import.meta.env.VITE_NODE_ENV !== 'development') { + Sentry.init({ + dsn: import.meta.env.VITE_SENTRY_DNS, + environment: import.meta.env.VITE_NODE_ENV, + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.replayIntegration(), + ], + + // Performance Monitoring + tracesSampleRate: 1.0, + tracePropagationTargets: ['localhost', /^https:\/\/api.snackga.me/], + + // Session Replay + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1.0, + }); + } }, []); ReactGA.send({ diff --git a/src/pages/games/SnackGame/game/components/ExpBarChart.tsx b/src/pages/games/SnackGame/game/components/ExpBarChart.tsx index 793e4134..a0e1f47d 100644 --- a/src/pages/games/SnackGame/game/components/ExpBarChart.tsx +++ b/src/pages/games/SnackGame/game/components/ExpBarChart.tsx @@ -10,7 +10,7 @@ import ChartDataLabels from 'chartjs-plugin-datalabels'; import { StatusType } from '@utils/types/member.type'; -import { TIER_COLOR } from '@constants/tier.constant'; +import { TIER_COLOR } from '@constants/common.constant'; import type { ChartOptions, Plugin } from 'chart.js'; diff --git a/src/pages/games/SnackGame/game/view/snackGame/SnackGameView.tsx b/src/pages/games/SnackGame/game/view/snackGame/SnackGameView.tsx index 347b911c..56f53f0e 100644 --- a/src/pages/games/SnackGame/game/view/snackGame/SnackGameView.tsx +++ b/src/pages/games/SnackGame/game/view/snackGame/SnackGameView.tsx @@ -120,7 +120,7 @@ const SnackGameView = ({ isOngoing, snackGame, onRemove }: SnackGameProps) => { return (
{isOngoing && ( ({ key: ATOM_KEY.MODAL,