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)
This commit is contained in:
parent
c67e57505a
commit
78e95f6073
@ -36,6 +36,7 @@
|
||||
"@types/rebass": "^4.0.7",
|
||||
"@types/styled-components": "^5.1.0",
|
||||
"@types/testing-library__cypress": "^5.0.5",
|
||||
"@types/ua-parser-js": "^0.7.35",
|
||||
"@types/wcag-contrast": "^3.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.1.0",
|
||||
"@typescript-eslint/parser": "^4.1.0",
|
||||
@ -105,6 +106,7 @@
|
||||
"styled-system": "^5.1.5",
|
||||
"typechain": "^4.0.3",
|
||||
"typescript": "^4.2.3",
|
||||
"ua-parser-js": "^0.7.28",
|
||||
"use-count-up": "^2.2.5",
|
||||
"wcag-contrast": "^3.0.0",
|
||||
"workbox-core": "^6.1.0",
|
||||
|
144
src/components/ErrorBoundary/index.tsx
Normal file
144
src/components/ErrorBoundary/index.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
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') {
|
||||
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() {
|
||||
return (
|
||||
<>
|
||||
|
@ -8,6 +8,7 @@ import Polling from '../components/Header/Polling'
|
||||
// import URLWarning from '../components/Header/URLWarning'
|
||||
import Popups from '../components/Popups'
|
||||
import Web3ReactManager from '../components/Web3ReactManager'
|
||||
import ErrorBoundary from '../components/ErrorBoundary'
|
||||
import { ApplicationModal } from '../state/application/actions'
|
||||
import { useModalOpen, useToggleModal } from '../state/application/hooks'
|
||||
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
|
||||
@ -72,62 +73,69 @@ function TopLevelModals() {
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<Route component={GoogleAnalyticsReporter} />
|
||||
<Route component={DarkModeQueryParamReader} />
|
||||
<AppWrapper>
|
||||
<HeaderWrapper>
|
||||
<Header />
|
||||
</HeaderWrapper>
|
||||
<BodyWrapper>
|
||||
<ThemedBackground />
|
||||
<Popups />
|
||||
<Polling />
|
||||
<TopLevelModals />
|
||||
<Web3ReactManager>
|
||||
<Switch>
|
||||
<Route exact strict path="/vote" component={Vote} />
|
||||
<Route exact strict path="/vote/:id" component={VotePage} />
|
||||
<Route exact strict path="/claim" component={OpenClaimAddressModalAndRedirectToSwap} />
|
||||
<Route exact strict path="/uni" component={Earn} />
|
||||
<Route exact strict path="/uni/:currencyIdA/:currencyIdB" component={Manage} />
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={null}>
|
||||
<Route component={GoogleAnalyticsReporter} />
|
||||
<Route component={DarkModeQueryParamReader} />
|
||||
<AppWrapper>
|
||||
<HeaderWrapper>
|
||||
<Header />
|
||||
</HeaderWrapper>
|
||||
<BodyWrapper>
|
||||
<ThemedBackground />
|
||||
<Popups />
|
||||
<Polling />
|
||||
<TopLevelModals />
|
||||
<Web3ReactManager>
|
||||
<Switch>
|
||||
<Route exact strict path="/vote" component={Vote} />
|
||||
<Route exact strict path="/vote/:id" component={VotePage} />
|
||||
<Route exact strict path="/claim" component={OpenClaimAddressModalAndRedirectToSwap} />
|
||||
<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="/swap/:outputCurrency" component={RedirectToSwap} />
|
||||
<Route exact strict path="/swap" component={Swap} />
|
||||
<Route exact strict path="/send" component={RedirectPathToSwapOnly} />
|
||||
<Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} />
|
||||
<Route exact strict path="/swap" component={Swap} />
|
||||
|
||||
<Route exact strict path="/find" component={PoolFinder} />
|
||||
<Route exact strict path="/pool/v2" component={PoolV2} />
|
||||
<Route exact strict path="/pool" component={Pool} />
|
||||
<Route exact strict path="/pool/:tokenId" component={PositionPage} />
|
||||
<Route exact strict path="/find" component={PoolFinder} />
|
||||
<Route exact strict path="/pool/v2" component={PoolV2} />
|
||||
<Route exact strict path="/pool" component={Pool} />
|
||||
<Route exact strict path="/pool/:tokenId" component={PositionPage} />
|
||||
|
||||
<Route exact strict path="/add/v2/:currencyIdA?/:currencyIdB?" component={RedirectDuplicateTokenIdsV2} />
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path="/add/:currencyIdA?/:currencyIdB?/:feeAmount?"
|
||||
component={RedirectDuplicateTokenIds}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path="/add/v2/:currencyIdA?/:currencyIdB?"
|
||||
component={RedirectDuplicateTokenIdsV2}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path="/add/:currencyIdA?/:currencyIdB?/:feeAmount?"
|
||||
component={RedirectDuplicateTokenIds}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path="/increase/:currencyIdA?/:currencyIdB?/:feeAmount?/:tokenId?"
|
||||
component={AddLiquidity}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path="/increase/:currencyIdA?/:currencyIdB?/:feeAmount?/:tokenId?"
|
||||
component={AddLiquidity}
|
||||
/>
|
||||
|
||||
<Route exact strict path="/remove/v2/:currencyIdA/:currencyIdB" component={RemoveLiquidity} />
|
||||
<Route exact strict path="/remove/:tokenId" component={RemoveLiquidityV3} />
|
||||
<Route exact strict path="/remove/v2/:currencyIdA/:currencyIdB" component={RemoveLiquidity} />
|
||||
<Route exact strict path="/remove/:tokenId" component={RemoveLiquidityV3} />
|
||||
|
||||
<Route exact strict path="/migrate/v2" component={MigrateV2} />
|
||||
<Route exact strict path="/migrate/v2/:address" component={MigrateV2Pair} />
|
||||
<Route exact strict path="/migrate/v2" component={MigrateV2} />
|
||||
<Route exact strict path="/migrate/v2/:address" component={MigrateV2Pair} />
|
||||
|
||||
<Route component={RedirectPathToSwapOnly} />
|
||||
</Switch>
|
||||
</Web3ReactManager>
|
||||
<Marginer />
|
||||
</BodyWrapper>
|
||||
</AppWrapper>
|
||||
</Suspense>
|
||||
<Route component={RedirectPathToSwapOnly} />
|
||||
</Switch>
|
||||
</Web3ReactManager>
|
||||
<Marginer />
|
||||
</BodyWrapper>
|
||||
</AppWrapper>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
6
src/utils/getUserAgent.ts
Normal file
6
src/utils/getUserAgent.ts
Normal file
@ -0,0 +1,6 @@
|
||||
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 @@
|
||||
"@testing-library/dom" "^7.11.0"
|
||||
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@*":
|
||||
version "3.13.0"
|
||||
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:
|
||||
resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
|
||||
integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
|
||||
|
Loading…
Reference in New Issue
Block a user