Commit 78e95f60 authored by jochenboesmans's avatar jochenboesmans Committed by GitHub

Add App-level error boundary, referring users to GitHub issue creation (#1464)

* Add App-level error boundary, referring users to GitHub issue creation on page crashes. (#1452)

* Class component is used as boundary since catching errors is apparently not yet possible with hooks.

* EventListener in window was removed and replaced by error boundary's error catch, which now fires a GA exception. The fields it passes are slightly different because React uses slightly different error types.

* Pre-filling issues with dynamic data is possible with POST requests to GitHub's API, but the GH web client seems to only support pre-fill based on templates. Therefore users still need to copy error info themselves.

* Prefill GitHub issues with crash data.

* Added package 'react-device-detect' to include device data such as OS, browser etc. in crash report.
* Included error stack in issue body.
* Used <code> html tag for displaying stack to user.

* Slightly reduce vertical padding on code block.

* Add ua-parser-js for parsing user agent.

* Revert react-device-detect to ^1.6.2 (which is used for mobile detection etc. in components)
parent c67e5750
import React, { ErrorInfo } from 'react'
import { ExternalLink, ThemedBackground, TYPE } from '../../theme'
import { AutoColumn } from '../Column'
import styled from 'styled-components'
import ReactGA from 'react-ga'
import { getUserAgent } from '../../utils/getUserAgent'
const FallbackWrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
align-items: center;
z-index: 1;
`
const BodyWrapper = styled.div<{ margin?: string }>`
position: relative;
margin-top: 1rem;
max-width: 60%;
width: 100%;
`
const CodeBlockWrapper = styled.div`
background: ${({ theme }) => theme.bg0};
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
0px 24px 32px rgba(0, 0, 0, 0.01);
border-radius: 24px;
padding: 18px 24px;
color: ${({ theme }) => theme.text1};
`
const LinkWrapper = styled.div`
color: ${({ theme }) => theme.blue1};
padding: 6px 24px;
`
const SomethingWentWrongWrapper = styled.div`
padding: 6px 24px;
`
type ErrorBoundaryState = {
error: Error | null
}
export default class ErrorBoundary extends React.Component<unknown, ErrorBoundaryState> {
constructor(props: unknown) {
super(props)
this.state = { error: null }
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { error }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
ReactGA.exception({
...error,
...errorInfo,
fatal: true,
})
}
render() {
const { error } = this.state
if (error !== null) {
const encodedBody = encodeURIComponent(issueBody(error))
return (
<FallbackWrapper>
<ThemedBackground />
<BodyWrapper>
<AutoColumn gap={'md'}>
<SomethingWentWrongWrapper>
<TYPE.label fontSize={24} fontWeight={600}>
Something went wrong
</TYPE.label>
</SomethingWentWrongWrapper>
<CodeBlockWrapper>
<code>
<TYPE.main fontSize={10}>{error.stack}</TYPE.main>
</code>
</CodeBlockWrapper>
<LinkWrapper>
<ExternalLink
id={`create-github-issue-link`}
href={`https://github.com/Uniswap/uniswap-interface/issues/new?assignees=&labels=bug&body=${encodedBody}&title=Crash report`}
target="_blank"
>
<TYPE.link fontSize={16}>
Create an issue on GitHub
<span></span>
</TYPE.link>
</ExternalLink>
</LinkWrapper>
</AutoColumn>
</BodyWrapper>
</FallbackWrapper>
)
}
return this.props.children
}
}
function issueBody(error: Error): string {
if (!error) throw new Error('no error to report')
const deviceData = getUserAgent()
return `**Bug Description**
App crashed
**Steps to Reproduce**
1. Go to ...
2. Click on ...
...
${
error.name &&
`**Error**
\`\`\`
${error.name}${error.message && `: ${error.message}`}
\`\`\`
`
}
${
error.stack &&
`**Stacktrace**
\`\`\`
${error.stack}
\`\`\`
`
}
${
deviceData &&
`**Device data**
\`\`\`json5
${JSON.stringify(deviceData, null, 2)}
\`\`\`
`
}
`
}
...@@ -46,13 +46,6 @@ if (typeof GOOGLE_ANALYTICS_ID === 'string') { ...@@ -46,13 +46,6 @@ if (typeof GOOGLE_ANALYTICS_ID === 'string') {
ReactGA.initialize('test', { testMode: true, debug: true }) ReactGA.initialize('test', { testMode: true, debug: true })
} }
window.addEventListener('error', (error) => {
ReactGA.exception({
description: `${error.message} @ ${error.filename}:${error.lineno}:${error.colno}`,
fatal: true,
})
})
function Updaters() { function Updaters() {
return ( return (
<> <>
......
...@@ -8,6 +8,7 @@ import Polling from '../components/Header/Polling' ...@@ -8,6 +8,7 @@ import Polling from '../components/Header/Polling'
// import URLWarning from '../components/Header/URLWarning' // import URLWarning from '../components/Header/URLWarning'
import Popups from '../components/Popups' import Popups from '../components/Popups'
import Web3ReactManager from '../components/Web3ReactManager' import Web3ReactManager from '../components/Web3ReactManager'
import ErrorBoundary from '../components/ErrorBoundary'
import { ApplicationModal } from '../state/application/actions' import { ApplicationModal } from '../state/application/actions'
import { useModalOpen, useToggleModal } from '../state/application/hooks' import { useModalOpen, useToggleModal } from '../state/application/hooks'
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader' import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
...@@ -72,62 +73,69 @@ function TopLevelModals() { ...@@ -72,62 +73,69 @@ function TopLevelModals() {
export default function App() { export default function App() {
return ( return (
<Suspense fallback={null}> <ErrorBoundary>
<Route component={GoogleAnalyticsReporter} /> <Suspense fallback={null}>
<Route component={DarkModeQueryParamReader} /> <Route component={GoogleAnalyticsReporter} />
<AppWrapper> <Route component={DarkModeQueryParamReader} />
<HeaderWrapper> <AppWrapper>
<Header /> <HeaderWrapper>
</HeaderWrapper> <Header />
<BodyWrapper> </HeaderWrapper>
<ThemedBackground /> <BodyWrapper>
<Popups /> <ThemedBackground />
<Polling /> <Popups />
<TopLevelModals /> <Polling />
<Web3ReactManager> <TopLevelModals />
<Switch> <Web3ReactManager>
<Route exact strict path="/vote" component={Vote} /> <Switch>
<Route exact strict path="/vote/:id" component={VotePage} /> <Route exact strict path="/vote" component={Vote} />
<Route exact strict path="/claim" component={OpenClaimAddressModalAndRedirectToSwap} /> <Route exact strict path="/vote/:id" component={VotePage} />
<Route exact strict path="/uni" component={Earn} /> <Route exact strict path="/claim" component={OpenClaimAddressModalAndRedirectToSwap} />
<Route exact strict path="/uni/:currencyIdA/:currencyIdB" component={Manage} /> <Route exact strict path="/uni" component={Earn} />
<Route exact strict path="/uni/:currencyIdA/:currencyIdB" component={Manage} />
<Route exact strict path="/send" component={RedirectPathToSwapOnly} /> <Route exact strict path="/send" component={RedirectPathToSwapOnly} />
<Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} /> <Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} />
<Route exact strict path="/swap" component={Swap} /> <Route exact strict path="/swap" component={Swap} />
<Route exact strict path="/find" component={PoolFinder} /> <Route exact strict path="/find" component={PoolFinder} />
<Route exact strict path="/pool/v2" component={PoolV2} /> <Route exact strict path="/pool/v2" component={PoolV2} />
<Route exact strict path="/pool" component={Pool} /> <Route exact strict path="/pool" component={Pool} />
<Route exact strict path="/pool/:tokenId" component={PositionPage} /> <Route exact strict path="/pool/:tokenId" component={PositionPage} />
<Route exact strict path="/add/v2/:currencyIdA?/:currencyIdB?" component={RedirectDuplicateTokenIdsV2} /> <Route
<Route exact
exact strict
strict path="/add/v2/:currencyIdA?/:currencyIdB?"
path="/add/:currencyIdA?/:currencyIdB?/:feeAmount?" component={RedirectDuplicateTokenIdsV2}
component={RedirectDuplicateTokenIds} />
/> <Route
exact
strict
path="/add/:currencyIdA?/:currencyIdB?/:feeAmount?"
component={RedirectDuplicateTokenIds}
/>
<Route <Route
exact exact
strict strict
path="/increase/:currencyIdA?/:currencyIdB?/:feeAmount?/:tokenId?" path="/increase/:currencyIdA?/:currencyIdB?/:feeAmount?/:tokenId?"
component={AddLiquidity} component={AddLiquidity}
/> />
<Route exact strict path="/remove/v2/:currencyIdA/:currencyIdB" component={RemoveLiquidity} /> <Route exact strict path="/remove/v2/:currencyIdA/:currencyIdB" component={RemoveLiquidity} />
<Route exact strict path="/remove/:tokenId" component={RemoveLiquidityV3} /> <Route exact strict path="/remove/:tokenId" component={RemoveLiquidityV3} />
<Route exact strict path="/migrate/v2" component={MigrateV2} /> <Route exact strict path="/migrate/v2" component={MigrateV2} />
<Route exact strict path="/migrate/v2/:address" component={MigrateV2Pair} /> <Route exact strict path="/migrate/v2/:address" component={MigrateV2Pair} />
<Route component={RedirectPathToSwapOnly} /> <Route component={RedirectPathToSwapOnly} />
</Switch> </Switch>
</Web3ReactManager> </Web3ReactManager>
<Marginer /> <Marginer />
</BodyWrapper> </BodyWrapper>
</AppWrapper> </AppWrapper>
</Suspense> </Suspense>
</ErrorBoundary>
) )
} }
import { UAParser } from 'ua-parser-js'
export function getUserAgent(): UAParser.IResult {
const parser = new UAParser(window.navigator.userAgent)
return parser.getResult()
}
...@@ -3888,6 +3888,11 @@ ...@@ -3888,6 +3888,11 @@
"@testing-library/dom" "^7.11.0" "@testing-library/dom" "^7.11.0"
cypress "*" cypress "*"
"@types/ua-parser-js@^0.7.35":
version "0.7.35"
resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.35.tgz#cca67a95deb9165e4b1f449471801e6489d3fe93"
integrity sha512-PsPx0RLbo2Un8+ff2buzYJnZjzwhD3jQHPOG2PtVIeOhkRDddMcKU8vJtHpzzfLB95dkUi0qAkfLg2l2Fd0yrQ==
"@types/uglify-js@*": "@types/uglify-js@*":
version "3.13.0" version "3.13.0"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.0.tgz#1cad8df1fb0b143c5aba08de5712ea9d1ff71124" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.0.tgz#1cad8df1fb0b143c5aba08de5712ea9d1ff71124"
...@@ -18285,7 +18290,7 @@ typical@^2.6.0, typical@^2.6.1: ...@@ -18285,7 +18290,7 @@ typical@^2.6.0, typical@^2.6.1:
resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d" resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d"
integrity sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0= integrity sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=
ua-parser-js@^0.7.24: ua-parser-js@^0.7.24, ua-parser-js@^0.7.28:
version "0.7.28" version "0.7.28"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
......
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