diff --git a/namae.code-workspace b/namae.code-workspace index 2cb7696..64b4f15 100644 --- a/namae.code-workspace +++ b/namae.code-workspace @@ -1,9 +1,5 @@ { "folders": [ - { - "name": "Root", - "path": "." - }, { "name": "API", "path": "api" diff --git a/web/package.json b/web/package.json index e30ea4a..a073e36 100644 --- a/web/package.json +++ b/web/package.json @@ -5,11 +5,12 @@ "build": "NODE_ENV=production react-scripts build", "eject": "react-scripts eject", "now-build": "yarn build", - "now-dev": "BROWSER=none react-scripts start", - "start": "react-scripts start", + "now-dev": "NODE_ENV=development BROWSER=none react-scripts start", + "start": "NODE_ENV=development react-scripts start", "test": "react-scripts test" }, "dependencies": { + "@sentry/browser": "^5.6.3", "fetch-suspense": "^1.2.0", "i18next": ">=17.0.12", "i18next-browser-languagedetector": "^3.0.3", @@ -33,7 +34,7 @@ }, "devDependencies": { "@testing-library/jest-dom": "^4.1.0", - "@testing-library/react": "^9.1.3", + "@testing-library/react": "^9.1.4", "@types/i18next-node-fs-backend": "^2.1.0", "@types/jest": "^24.0.18", "@types/node": "^12.7.3", diff --git a/web/src/App.tsx b/web/src/App.tsx index b1b5d98..a213e00 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,7 +1,8 @@ -import React, {useState} from 'react'; +import React, {useState, useEffect} from 'react'; import styled, {createGlobalStyle} from 'styled-components'; import {Helmet} from 'react-helmet'; import {useTranslation} from 'react-i18next'; +import {initGA, sendQueryStatistics} from './util/analytics'; import Welcome from './components/Welcome'; import Form from './components/Form'; @@ -15,8 +16,13 @@ export default function App() { const [query, setQuery] = useState(''); const {t} = useTranslation(); + useEffect(() => { + initGA(); + }, []); + function onQuery(query: string) { setQuery(query); + sendQueryStatistics(query.length); } return ( diff --git a/web/src/components/cards/core.tsx b/web/src/components/cards/core.tsx index d2db96c..fe713a5 100644 --- a/web/src/components/cards/core.tsx +++ b/web/src/components/cards/core.tsx @@ -6,6 +6,7 @@ import 'react-tippy/dist/tippy.css'; import BarLoader from 'react-spinners/BarLoader'; import {GoInfo} from 'react-icons/go'; import {useTranslation} from 'react-i18next'; +import {sendError} from '../../util/analytics'; import {mobile} from '../../util/css'; import {ExternalLink} from '../Links'; @@ -21,16 +22,7 @@ export const Card: React.FC<{title: string}> = ({title, children}) => { {title} - - - - - }> - {children} - - + {children} ); @@ -55,12 +47,12 @@ export const Repeater: React.FC<{ return ( <> {items.map((name) => ( - {children(name)} + {children(name)} ))} {revealAlternatives ? moreItems.map((name) => ( - {children(name)} + {children(name)} )) : null} {moreItems.length > 0 && !revealAlternatives ? ( @@ -208,24 +200,37 @@ export const Result: React.FC<{ ); }; +// 1. getDerivedStateFromError +// 2. render() +// 3. componentDidCatch() send errorInfo to Sentry +// 4. render(), now with eventId provided from Sentry class ErrorBoundary extends React.Component< {}, - {hasError: boolean; message: string} + {hasError: boolean; message: string; eventId?: string} > { constructor(props: {}) { super(props); - this.state = {hasError: false, message: ''}; + this.state = {hasError: false, message: '', eventId: undefined}; } + // used in SSR static getDerivedStateFromError(error: Error) { return {hasError: true, message: error.message}; } + componentDidCatch(error: Error, errorInfo: any) { + sendError(error, errorInfo).then((eventId) => { + this.setState({eventId}); + }); + } + render() { if (this.state.hasError) { return ( ( +const ErrorHandler: React.FC = ({children}) => ( ( +initSentry(); + +ReactDOM.render( - + , + document.getElementById('root'), ); -ReactDOM.render(, document.getElementById('root')); - -// register Google Analytics -if (process.env.NODE_ENV !== 'development') { - import('react-ga').then((ReactGA) => { - ReactGA.initialize('UA-28919359-15'); - ReactGA.pageview(window.location.pathname + window.location.search); - }); -} - serviceWorker.register({}); diff --git a/web/src/util/analytics.ts b/web/src/util/analytics.ts new file mode 100644 index 0000000..00906c6 --- /dev/null +++ b/web/src/util/analytics.ts @@ -0,0 +1,41 @@ +import ReactGA from 'react-ga'; +import * as Sentry from '@sentry/browser'; + +const isProduction = process.env.NODE_ENV !== 'development'; + +export function initGA() { + if (isProduction) { + ReactGA.initialize('UA-28919359-15'); + ReactGA.pageview(window.location.pathname + window.location.search); + } +} + +export function sendQueryStatistics(queryLength: number) { + if (isProduction) { + ReactGA.event({ + category: 'Search', + action: 'New search invoked', + value: queryLength, + }); + } +} + +export function initSentry() { + if (isProduction) { + Sentry.init({ + dsn: 'https://7ab2df74aead499b950ebef190cc40b7@sentry.io/1759299', + }); + } +} + +export function sendError(error: Error, errorInfo: any): Promise { + return new Promise((resolve) => { + if (isProduction) { + Sentry.withScope((scope) => { + scope.setExtras(errorInfo); + const eventId = Sentry.captureException(error); + resolve(eventId); + }); + } + }); +} diff --git a/web/yarn.lock b/web/yarn.lock index f9cc0e8..98534bf 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1091,6 +1091,58 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== +"@sentry/browser@^5.6.3": + version "5.6.3" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.6.3.tgz#5cc37b0443eba55ad13c13d34d6b95ff30dfbfe3" + integrity sha512-bP1LTbcKPOkkmfJOAM6c7WZ0Ov0ZEW6B9keVZ9wH9fw/lBPd9UyDMDCwJ+FAYKz9M9S5pxQeJ4Ebd7WUUrGVAQ== + dependencies: + "@sentry/core" "5.6.2" + "@sentry/types" "5.6.1" + "@sentry/utils" "5.6.1" + tslib "^1.9.3" + +"@sentry/core@5.6.2": + version "5.6.2" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.6.2.tgz#8c5477654a83ebe41a72e86a79215deb5025e418" + integrity sha512-grbjvNmyxP5WSPR6UobN2q+Nss7Hvz+BClBT8QTr7VTEG5q89TwNddn6Ej3bGkaUVbct/GpVlI3XflWYDsnU6Q== + dependencies: + "@sentry/hub" "5.6.1" + "@sentry/minimal" "5.6.1" + "@sentry/types" "5.6.1" + "@sentry/utils" "5.6.1" + tslib "^1.9.3" + +"@sentry/hub@5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.6.1.tgz#9f355c0abcc92327fbd10b9b939608aa4967bece" + integrity sha512-m+OhkIV5yTAL3R1+XfCwzUQka0UF/xG4py8sEfPXyYIcoOJ2ZTX+1kQJLy8QQJ4RzOBwZA+DzRKP0cgzPJ3+oQ== + dependencies: + "@sentry/types" "5.6.1" + "@sentry/utils" "5.6.1" + tslib "^1.9.3" + +"@sentry/minimal@5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.6.1.tgz#09d92b26de0b24555cd50c3c33ba4c3e566009a1" + integrity sha512-ercCKuBWHog6aS6SsJRuKhJwNdJ2oRQVWT2UAx1zqvsbHT9mSa8ZRjdPHYOtqY3DoXKk/pLUFW/fkmAnpdMqRw== + dependencies: + "@sentry/hub" "5.6.1" + "@sentry/types" "5.6.1" + tslib "^1.9.3" + +"@sentry/types@5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.6.1.tgz#5915e1ee4b7a678da3ac260c356b1cb91139a299" + integrity sha512-Kub8TETefHpdhvtnDj3kKfhCj0u/xn3Zi2zIC7PB11NJHvvPXENx97tciz4roJGp7cLRCJsFqCg4tHXniqDSnQ== + +"@sentry/utils@5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.6.1.tgz#69d9e151e50415bc91f2428e3bcca8beb9bc2815" + integrity sha512-rfgha+UsHW816GqlSRPlniKqAZylOmQWML2JsujoUP03nPu80zdN43DK9Poy/d9OxBxv0gd5K2n+bFdM2kqLQQ== + dependencies: + "@sentry/types" "5.6.1" + tslib "^1.9.3" + "@sheerun/mutationobserver-shim@^0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b" @@ -1199,10 +1251,10 @@ "@svgr/plugin-svgo" "^4.3.1" loader-utils "^1.2.3" -"@testing-library/dom@^6.0.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-6.1.0.tgz#8d5a954158e81ecd7c994907f4ec240296ed823b" - integrity sha512-qivqFvnbVIH3DyArFofEU/jlOhkGIioIemOy9A9M/NQTpPyDDQmtVkAfoB18RKN581f0s/RJMRBbq9WfMIhFTw== +"@testing-library/dom@^6.1.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-6.4.0.tgz#aaf7fceba1272516fc7c5ac0716a24f63ea6cdaa" + integrity sha512-uQFwl+mIH9THk9Q9qVZKBgoL/6ahVEQu9bDeOmY5yB8uc62L2Z9eYs0g7zNTdMsg4I0bOdPPMs/sNETYP5+PEw== dependencies: "@babel/runtime" "^7.5.5" "@sheerun/mutationobserver-shim" "^0.3.2" @@ -1226,13 +1278,13 @@ pretty-format "^24.0.0" redent "^3.0.0" -"@testing-library/react@^9.1.3": - version "9.1.3" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-9.1.3.tgz#3fb495227322ea36cd817532441dabb552e0d6ce" - integrity sha512-qFVo6TsEbpEFpOmKjIxMHDujOKVdvVpcYFcUfJeWBqMO8eja5pN9SZnt6W6AzW3a1MRvRfw3X0Fhx3eXnBJxjA== +"@testing-library/react@^9.1.4": + version "9.1.4" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-9.1.4.tgz#4cc1a228a944c0f468ee501e7da1651d8bbd9902" + integrity sha512-fQ/PXZoLcmnS1W5ZiM3P7XBy2x6Hm9cJAT/ZDuZKzJ1fS1rN3j31p7ReAqUe3N1kJ46sNot0n1oiGbz7FPU+FA== dependencies: "@babel/runtime" "^7.5.5" - "@testing-library/dom" "^6.0.0" + "@testing-library/dom" "^6.1.0" "@types/testing-library__react" "^9.1.0" "@types/babel__core@^7.1.0": @@ -9829,7 +9881,7 @@ ts-pnp@^1.1.2: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.4.tgz#ae27126960ebaefb874c6d7fa4729729ab200d90" integrity sha512-1J/vefLC+BWSo+qe8OnJQfWTYRS6ingxjwqmHMqaMxXMj7kFtKLgAaYW3JeX3mktjgUL+etlU8/B4VUAUI9QGw== -tslib@^1.8.1, tslib@^1.9.0: +tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==