Skip to content

Commit

Permalink
feat(ErrorHandler): handle react errors, crash reports
Browse files Browse the repository at this point in the history
We've been getting complaints from users of a "blank white screen", which is caused by unhandled exceptions in react code.

While we're learning from past lessons, we've continually gotten incredible ROI for time spent building tools that log and report errors. To that end, I've added a crash reporter to our error handler. Ideally this'll help us diagnose and address fixes faster without forcing users to send us data.
  • Loading branch information
b5 committed Mar 2, 2020
1 parent e5c213b commit 22480ea
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 2 deletions.
119 changes: 119 additions & 0 deletions app/components/ErrorHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React from 'react'
import versions from '../../version'
import { CRASH_REPORTER_URL } from '../constants'

import localStore from '../utils/localStore'
import ExternalLink from './ExternalLink'
import Button from './chrome/Button'

export default class ErrorHandler extends React.Component {
state = {
error: undefined,
errorInfo: undefined,
sendingReport: false,
sentReport: false
}

constructor (props) {
super(props)

this.handleSendCrashReport = this.handleSendCrashReport.bind(this)
this.handleReload = this.handleReload.bind(this)
}

componentDidCatch (error, errorInfo) {
this.setState({ error, errorInfo, sendingReport: false })

// clear local storage, which may cause re-crashing if in an error put us
// into an un-recoverable state
localStore().setItem('peername', '')
localStore().setItem('name', '')
localStore().setItem('activeTab', 'status')
localStore().setItem('component', '')
localStore().setItem('commitComponent', '')
}

handleSendCrashReport (e: React.MouseEvent) {
const { error, errorInfo } = this.state
this.setState({ error, errorInfo, sendingReport: true })

console.log('sending report')
postCrashReport(error, errorInfo)
.then(() => {
this.setState({
sentReport: true,
sendingReport: false
})
})
.catch((e) => {
console.log(e)
this.setState({
sendingReport: false
})
})
}

handleReload (e: React.MouseEvent) {
window.location.hash = '/'
window.location.reload()
}

render () {
if (!this.state.error) {
return this.props.children
}

const { sendingReport, sentReport } = this.state

return (
<div className="error-container">
<div className="dialog">
<h1>Dang. It broke.</h1>
<p>Apologies, Qri desktop encountered an error. Send us a crash report!</p>
<div className="reporting actions">
{sentReport
? <p>Thanks!</p>
: <Button
id="send-crash-report"
text="Send Crash Report"
color="primary"
loading={sendingReport}
onClick={this.handleSendCrashReport}
/>}
</div>
<div className="reload">
<p>If you have additional info or questions, feel free to <ExternalLink id="file-issue" href="https://github.com/qri-io/desktop/issues/new">file an issue</ExternalLink> describing where things went wrong. The more detail, the better. Reload to get back to Qri.</p>
<Button
id="error-reload"
text="Reload Qri Desktop"
color="dark"
onClick={this.handleReload}
/>
</div>
</div>
</div>
)
}
}

// TODO (b5) - bring this back in the near future for fetching home feed
async function postCrashReport (err, errInfo): Promise<any> {
const options: FetchOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
app: 'desktop',
version: versions.desktopVersion,
platform: navigator.platform,
error: err.toString(),
info: errInfo.componentStack
})
}

console.log(CRASH_REPORTER_URL)
const r = await fetch(CRASH_REPORTER_URL, options)
const res = await r.json()
return res
}
4 changes: 3 additions & 1 deletion app/constants.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// these constants may be used by both the main process and frontend
// so they need to be defined as CommonJS
const BACKEND_URL = 'http://localhost:2503'
const CRASH_REPORTER_URL = 'https://crashreports.qri.io/desktop'
const DISCORD_URL = 'https://discordapp.com/invite/thkJHKj'
const QRI_CLOUD_URL = 'https://qri.cloud'
const BACKEND_URL = 'http://localhost:2503'
const WEBSOCKETS_URL = 'ws://localhost:2506'
const WEBSOCKETS_PROTOCOL = 'qri-websocket'
// 3000ms is quick enough for the app to feel responsive
Expand All @@ -11,6 +12,7 @@ const DEFAULT_POLL_INTERVAL = 3000

module.exports = {
BACKEND_URL,
CRASH_REPORTER_URL,
DEFAULT_POLL_INTERVAL,
DISCORD_URL,
QRI_CLOUD_URL,
Expand Down
5 changes: 4 additions & 1 deletion app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react'
import * as ReactDOM from 'react-dom'
import 'regenerator-runtime/runtime'
import { Provider } from 'react-redux'
import ErrorHandler from './components/ErrorHandler'
import AppContainer from './containers/AppContainer'

import './app.global.scss'
Expand All @@ -14,7 +15,9 @@ const { store } = require('./store/configureStore') // eslint-disable-line

ReactDOM.render(
<Provider store={store}>
<AppContainer />
<ErrorHandler>
<AppContainer />
</ErrorHandler>
</Provider>,
document.getElementById('root')
)
19 changes: 19 additions & 0 deletions app/scss/0.4.0/errorHandler.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

.error-container {
width: 100%;
height: 100%;

.dialog {
margin: 0 auto;
padding-top: 8em;
width: 350px;

.reporting.actions button {
margin-right: 15px;
}

.reload {
margin-top: 35px;
}
}
}
1 change: 1 addition & 0 deletions app/scss/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

@import "0.4.0/icons";
@import "0.4.0/chrome";
@import "0.4.0/errorHandler";
@import "0.4.0/type";
@import "0.4.0/nav";
@import "0.4.0/item";
Expand Down

0 comments on commit 22480ea

Please sign in to comment.