Commit 0e9b0540 authored by Mike Grabowski's avatar Mike Grabowski Committed by GitHub

feat: add Sentry / improve error handling (#5509)

* initial commit

* chore: tweaks

* chore: tweaks ahead of full rewrite

* chore: only enable Sentry in production builds

* chore: keep existing behavior

* chore: fix lint

* chore: add release

* feat: remove GA sendEvent exception

* chore: simplify

* chore: bring back new line

* chore: group code together
parent 55d85d26
......@@ -2,4 +2,5 @@ REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_AWS_API_REGION="us-east-2"
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
\ No newline at end of file
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
\ No newline at end of file
import { Trans } from '@lingui/macro'
import { sendEvent } from 'components/analytics'
import React, { ErrorInfo, PropsWithChildren } from 'react'
import * as Sentry from '@sentry/react'
import React, { PropsWithChildren } from 'react'
import styled from 'styled-components/macro'
import store, { AppState } from '../../state'
import { ExternalLink, ThemedText } from '../../theme'
import { userAgent } from '../../utils/userAgent'
import { AutoColumn } from '../Column'
import { AutoRow } from '../Row'
......@@ -33,21 +31,41 @@ const CodeBlockWrapper = styled.div`
color: ${({ theme }) => theme.deprecated_text1};
`
const LinkWrapper = styled.div`
color: ${({ theme }) => theme.deprecated_blue1};
const Padding = styled.div`
padding: 6px 24px;
`
const SomethingWentWrongWrapper = styled.div`
padding: 6px 24px;
`
type ErrorBoundaryState = {
error: Error | null
const Fallback = ({ error }: { error: Error }) => {
return (
<FallbackWrapper>
<BodyWrapper>
<AutoColumn gap="md">
<Padding>
<ThemedText.DeprecatedLabel fontSize={24} fontWeight={600}>
<Trans>Something went wrong</Trans>
</ThemedText.DeprecatedLabel>
</Padding>
<CodeBlockWrapper>
<code>
<ThemedText.DeprecatedMain fontSize={10}>{error.stack}</ThemedText.DeprecatedMain>
</code>
</CodeBlockWrapper>
<AutoRow>
<Padding>
<ExternalLink id="get-support-on-discord" href="https://discord.gg/FCfyBSbCU5" target="_blank">
<ThemedText.DeprecatedLink fontSize={16} color="deprecated_blue1">
<Trans>Get support on Discord</Trans>
<span></span>
</ThemedText.DeprecatedLink>
</ExternalLink>
</Padding>
</AutoRow>
</AutoColumn>
</BodyWrapper>
</FallbackWrapper>
)
}
const IS_UNISWAP = window.location.hostname === 'app.uniswap.org'
async function updateServiceWorker(): Promise<ServiceWorkerRegistration> {
const ready = await navigator.serviceWorker.ready
// the return type of update is incorrectly typed as Promise<void>. See
......@@ -55,157 +73,36 @@ async function updateServiceWorker(): Promise<ServiceWorkerRegistration> {
return ready.update() as unknown as Promise<ServiceWorkerRegistration>
}
export default class ErrorBoundary extends React.Component<PropsWithChildren<unknown>, ErrorBoundaryState> {
constructor(props: PropsWithChildren<unknown>) {
super(props)
this.state = { error: null }
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
updateServiceWorker()
.then(async (registration) => {
// We want to refresh only if we detect a new service worker is waiting to be activated.
// See details about it: https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle
if (registration?.waiting) {
await registration.unregister()
// Makes Workbox call skipWaiting(). For more info on skipWaiting see: https://developer.chrome.com/docs/workbox/handling-service-worker-updates/
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
const reloadIfUpdateAvailable = async () => {
try {
const registration = await updateServiceWorker()
// Once the service worker is unregistered, we can reload the page to let
// the browser download a fresh copy of our app (invalidating the cache)
window.location.reload()
}
})
.catch((error) => {
console.error('Failed to update service worker', error)
})
return { error }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
sendEvent('exception', {
description: error.toString() + errorInfo.toString(),
fatal: true,
})
}
// We want to refresh only if we detect a new service worker is waiting to be activated.
// See details about it: https://web.dev/service-worker-lifecycle/
if (registration?.waiting) {
await registration.unregister()
render() {
const { error } = this.state
// Makes Workbox call skipWaiting().
// For more info on skipWaiting see: https://web.dev/service-worker-lifecycle/#skip-the-waiting-phase
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
if (error !== null) {
const encodedBody = encodeURIComponent(issueBody(error))
return (
<FallbackWrapper>
<BodyWrapper>
<AutoColumn gap="md">
<SomethingWentWrongWrapper>
<ThemedText.DeprecatedLabel fontSize={24} fontWeight={600}>
<Trans>Something went wrong</Trans>
</ThemedText.DeprecatedLabel>
</SomethingWentWrongWrapper>
<CodeBlockWrapper>
<code>
<ThemedText.DeprecatedMain fontSize={10}>{error.stack}</ThemedText.DeprecatedMain>
</code>
</CodeBlockWrapper>
{IS_UNISWAP ? (
<AutoRow>
<LinkWrapper>
<ExternalLink
id="create-github-issue-link"
href={`https://github.com/Uniswap/uniswap-interface/issues/new?assignees=&labels=bug&body=${encodedBody}&title=${encodeURIComponent(
`Crash report: \`${error.name}${error.message && `: ${error.message}`}\``
)}`}
target="_blank"
>
<ThemedText.DeprecatedLink fontSize={16}>
<Trans>Create an issue on GitHub</Trans>
<span></span>
</ThemedText.DeprecatedLink>
</ExternalLink>
</LinkWrapper>
<LinkWrapper>
<ExternalLink id="get-support-on-discord" href="https://discord.gg/FCfyBSbCU5" target="_blank">
<ThemedText.DeprecatedLink fontSize={16}>
<Trans>Get support on Discord</Trans>
<span></span>
</ThemedText.DeprecatedLink>
</ExternalLink>
</LinkWrapper>
</AutoRow>
) : null}
</AutoColumn>
</BodyWrapper>
</FallbackWrapper>
)
window.location.reload()
}
return this.props.children
}
}
function getRelevantState(): null | keyof AppState {
const path = window.location.hash
if (!path.startsWith('#/')) {
return null
}
const pieces = path.substring(2).split(/[/\\?]/)
switch (pieces[0]) {
case 'swap':
return 'swap'
case 'add':
if (pieces[1] === 'v2') return 'mint'
else return 'mintV3'
case 'remove':
if (pieces[1] === 'v2') return 'burn'
else return 'burnV3'
} catch (error) {
console.error('Failed to update service worker', error)
}
return null
}
function issueBody(error: Error): string {
const relevantState = getRelevantState()
const deviceData = userAgent
return `## URL
${window.location.href}
${
relevantState
? `## \`${relevantState}\` state
\`\`\`json
${JSON.stringify(store.getState()[relevantState], null, 2)}
\`\`\`
`
: ''
}
${
error.name &&
`## Error
\`\`\`
${error.name}${error.message && `: ${error.message}`}
\`\`\`
`
}
${
error.stack &&
`## Stacktrace
\`\`\`
${error.stack}
\`\`\`
`
}
${
deviceData &&
`## Device data
\`\`\`json
${JSON.stringify(deviceData, null, 2)}
\`\`\`
`
}
`
export default function ErrorBoundary({ children }: PropsWithChildren): JSX.Element {
return (
<Sentry.ErrorBoundary
fallback={Fallback}
beforeCapture={(scope) => {
scope.setLevel('fatal')
}}
onError={reloadIfUpdateAvailable}
>
{children}
</Sentry.ErrorBoundary>
)
}
import { Trans } from '@lingui/macro'
import * as Sentry from '@sentry/react'
import { Currency, Price, Token } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { sendEvent } from 'components/analytics'
import { AutoColumn, ColumnCenter } from 'components/Column'
import Loader from 'components/Loader'
import { format } from 'd3'
......@@ -157,7 +157,7 @@ export default function LiquidityChartRangeInput({
)
if (error) {
sendEvent('exception', { description: error.toString(), fatal: false })
Sentry.captureMessage(error.toString(), 'log')
}
const isUninitialized = !currencyA || !currencyB || (formattedData === undefined && !isLoading)
......
......@@ -15,7 +15,6 @@ const AirdropModal = lazy(() => import('components/AirdropModal'))
export default function TopLevelModals() {
const addressClaimOpen = useModalIsOpen(ApplicationModal.ADDRESS_CLAIM)
const addressClaimToggle = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT)
const { account } = useWeb3React()
const location = useLocation()
......@@ -23,7 +22,6 @@ export default function TopLevelModals() {
location.pathname.startsWith('/swap') ||
location.pathname.startsWith('/tokens') ||
location.pathname.startsWith('/pool')
useAccountRiskCheck(account)
const open = Boolean(blockedAccountModalOpen && account)
return (
......
import * as Sentry from '@sentry/react'
import { Currency, Token } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { sendEvent } from 'components/analytics'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import ms from 'ms.macro'
import { useMemo } from 'react'
......@@ -89,7 +89,7 @@ function usePoolTVL(token0: Token | undefined, token1: Token | undefined) {
}
if (latestBlock - (_meta?.block?.number ?? 0) > MAX_DATA_BLOCK_AGE) {
sendEvent('exception', { description: `Graph stale (latest block: ${latestBlock})` })
Sentry.captureMessage(`Graph stale (latest block: ${latestBlock})`, 'log')
return {
isLoading,
......
......@@ -3,6 +3,7 @@ import 'inter-ui'
import 'polyfills'
import 'components/analytics'
import * as Sentry from '@sentry/react'
import { FeatureFlagsProvider } from 'featureFlags'
import RelayEnvironment from 'graphql/data/RelayEnvironment'
import { BlockNumberProvider } from 'lib/hooks/useBlockNumber'
......@@ -13,6 +14,7 @@ import { QueryClient, QueryClientProvider } from 'react-query'
import { Provider } from 'react-redux'
import { RelayEnvironmentProvider } from 'react-relay'
import { HashRouter } from 'react-router-dom'
import { isProductionEnv } from 'utils/env'
import Web3Provider from './components/Web3Provider'
import { LanguageProvider } from './i18n'
......@@ -27,12 +29,17 @@ import UserUpdater from './state/user/updater'
import ThemeProvider, { ThemedGlobalStyle } from './theme'
import RadialGradientByChainUpdater from './theme/components/RadialGradientByChainUpdater'
const queryClient = new QueryClient()
if (!!window.ethereum) {
window.ethereum.autoRefreshOnNetworkChange = false
}
if (isProductionEnv()) {
Sentry.init({
dsn: process.env.REACT_APP_SENTRY_DSN,
release: process.env.REACT_APP_GIT_COMMIT_HASH,
})
}
function Updaters() {
return (
<>
......@@ -47,6 +54,8 @@ function Updaters() {
)
}
const queryClient = new QueryClient()
const container = document.getElementById('root') as HTMLElement
createRoot(container).render(
......
......@@ -2974,6 +2974,49 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"
"@sentry/browser@7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.23.0.tgz#ca2a01ce2b00727036906158efaa1c7af1395cc0"
integrity sha512-2/dLGOSaM5AvlRdMgYxDyxPxkUUqYyxF7QZ0NicdIXkKXa0fM38IdibeXrE8XzC7rF2B7DQZ6U7uDb1Yry60ig==
dependencies:
"@sentry/core" "7.23.0"
"@sentry/types" "7.23.0"
"@sentry/utils" "7.23.0"
tslib "^1.9.3"
"@sentry/core@7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.23.0.tgz#d320b2b6e5620b41f345bc01d69b547cdf28f78d"
integrity sha512-oNLGsscSdMs1urCbpwe868NsoJWyeTOQXOm5w2e78yE7G6zm2Ra473NQio3lweaEvjQgSGpFyEfAn/3ubZbtPw==
dependencies:
"@sentry/types" "7.23.0"
"@sentry/utils" "7.23.0"
tslib "^1.9.3"
"@sentry/react@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.23.0.tgz#950487365a696d2506c3103f44be8a64f0e79cda"
integrity sha512-MY8WhhImMYEbF8YMPHxs/fhQ9DftmWz1KxT52jEcZUAYfbwmt8zVy4LfBUpMNA4rFy72E9k4DsFQtD0FwCcp3g==
dependencies:
"@sentry/browser" "7.23.0"
"@sentry/types" "7.23.0"
"@sentry/utils" "7.23.0"
hoist-non-react-statics "^3.3.2"
tslib "^1.9.3"
"@sentry/types@7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.23.0.tgz#5d2ce94d81d7c1fad702645306f3c0932708cad5"
integrity sha512-fZ5XfVRswVZhKoCutQ27UpIHP16tvyc6ws+xq+njHv8Jg8gFBCoOxlJxuFhegD2xxylAn1aiSHNAErFWdajbpA==
"@sentry/utils@7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.23.0.tgz#5f38640fe49f5abac88f048b92d3e83375d7ddf7"
integrity sha512-ad/XXH03MfgDH/7N7FjKEOVaKrfQWdMaE0nCxZCr2RrvlitlmGQmPpms95epr1CpzSU3BDRImlILx6+TlrXOgg==
dependencies:
"@sentry/types" "7.23.0"
tslib "^1.9.3"
"@sinonjs/commons@^1.7.0":
version "1.8.3"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment