Skip to content

Commit

Permalink
Add logging and user reporting to the frontend.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jonesy committed Feb 11, 2020
1 parent 033ca10 commit 50a2f61
Show file tree
Hide file tree
Showing 13 changed files with 355 additions and 92 deletions.
221 changes: 144 additions & 77 deletions frontend/package-lock.json

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions frontend/server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ const bodyParser = require('body-parser');
const config = require('config');
const cookieParser = require('cookie-parser');
const express = require('express');
const fs = require('fs');
const isFunction = require('lodash/isFunction');
const get = require('lodash/get');
const path = require('path');
const passport = require('passport');
const session = require('express-session');
const manifestHelpers = require('express-manifest-helpers-upgraded');
const MemoryStore = require('memorystore')(session);
const morgan = require('morgan');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
Expand Down Expand Up @@ -43,6 +45,11 @@ const repositoryHost = config.get('repositoryHost');
const memoryStore = new MemoryStore({
checkPeriod: 86400000, // prune expired entries every 24h
});
const logger = morgan('common', {
stream: fs.createWriteStream(path.join(__dirname, 'logs', 'frontend.log'), {
flags: 'a',
}),
});

if (isDevelopment) {
const compiler = webpack(webpackConfig);
Expand Down Expand Up @@ -77,6 +84,7 @@ app.use(
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, '..', 'dist')));
app.use(logger);

// Auth & Session
app.use(cookieParser(cookieSecret));
Expand Down Expand Up @@ -106,6 +114,20 @@ app.get('/hello', (req, res) => {
res.status(200).send('hi');
});

app.post('/log', (req, res) => {
const { state, action } = req.body;
const date = new Date().toString();

fs.writeFile(
path.join(__dirname, 'logs', `state-${date}.log`),
JSON.stringify({
state,
action,
}),
() => res.send('Log Received')
);
});

app.get('*', checkAuth, storeUrl, (req, res) => {
res.render('index', {
isDevelopment,
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/components/app-bar/menu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import Avatar from '@atlaskit/avatar';
import Dropdown, { DropdownItem } from '@atlaskit/dropdown-menu';
import isEmpty from 'lodash/isEmpty';
import aboutButton from '@src/modules/app/containers/about-button';
import reportErrorButton from '@src/modules/app/containers/report-error-button';

const AboutDropdownItem = props => (
<DropdownItem {...props}>About this App</DropdownItem>
);
const ReportErrorDropdownItem = props => (
<DropdownItem {...props}>Report an Error</DropdownItem>
);
const AboutButton = aboutButton(AboutDropdownItem);
const ReportErrorButton = reportErrorButton(ReportErrorDropdownItem);

function AppBarMenu({ children, helpURL, user }) {
const possibleDisplayNameValues = at(user, [
Expand All @@ -35,6 +40,7 @@ function AppBarMenu({ children, helpURL, user }) {
</DropdownItem>
)}
{children}
<ReportErrorButton />
<AboutButton />
<DropdownItem href="/auth/logout">Logout</DropdownItem>
</Dropdown>
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/modules/app/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ export const fetchToken = () => ({
type: 'app/get/token',
});

export const toggleReportError = () => ({
type: 'app/report-error/toggle',
});

export const reportError = payload => ({
type: 'user/report-error',
error: true,
payload,
});

export const toggleAbout = () => ({
type: 'app/about/toggle',
});
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/modules/app/components/app/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
reportsGroup,
exporterMode,
} from '@src/services/config';
import ReportError from '@src/modules/app/containers/report-error';
import '@atlaskit/css-reset';

import About from '../../containers/about';
Expand Down Expand Up @@ -98,6 +99,7 @@ class App extends React.Component {
return (
<LayerManager>
<main id="app-main" className={styles.main}>
<ReportError />
<About />
<Messages />
<Auth
Expand Down
55 changes: 55 additions & 0 deletions frontend/src/modules/app/components/report-error/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as React from 'react';
import Button from '@atlaskit/button';
import Form, { Field } from '@atlaskit/form';
import ModalDialog, {
ModalFooter,
ModalTransition,
} from '@atlaskit/modal-dialog';
import Textarea from '@atlaskit/textarea';

function ReportError({ open, onCancel, onSubmit }) {
const Container = ({ children, className }) => (
<Form onSubmit={data => onSubmit(data)}>
{({ formProps }) => (
<form {...formProps} className={className}>
{children}
</form>
)}
</Form>
);
const Footer = () => (
<ModalFooter>
<Button onClick={onCancel}>Cancel</Button>
<Button appearance="primary" type="submit">
Submit
</Button>
</ModalFooter>
);

return (
<ModalTransition>
{open && (
<ModalDialog
components={{ Container, Footer }}
heading="Report an Error"
>
<Field
isRequired
label="Error Description"
name="error"
defaultValue=""
>
{({ fieldProps }) => (
<Textarea
{...fieldProps}
placeholder="Enter any details that you think might be relevant"
/>
)}
</Field>
</ModalDialog>
)}
</ModalTransition>
);
}

export default ReportError;
12 changes: 12 additions & 0 deletions frontend/src/modules/app/containers/report-error-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { connect } from 'react-redux';

import { toggleReportError } from '../actions';

// NOTE: This container just returns the connect function, add it to a button element
// where ever it is needed i.e. `const Button = reportErrorButton(CustomButtonElement)`
export default connect(
null,
{
onClick: toggleReportError,
}
);
16 changes: 16 additions & 0 deletions frontend/src/modules/app/containers/report-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import ReportError from '../components/report-error';

import { reportError, toggleReportError } from '../actions';

const mapStateToProps = state => ({
open: state.app.viewState.isReportErrorOpen,
});

export default connect(
mapStateToProps,
{
onCancel: toggleReportError,
onSubmit: reportError,
}
)(ReportError);
13 changes: 13 additions & 0 deletions frontend/src/modules/app/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { combineReducers } from 'redux';

const initialViewState = {
isAboutOpen: false,
isReportErrorOpen: false,
};

function viewState(state = initialViewState, action) {
Expand All @@ -12,6 +13,18 @@ function viewState(state = initialViewState, action) {
isAboutOpen: !state.isAboutOpen,
};

case 'app/report-error/toggle':
return {
...state,
isReportErrorOpen: !state.isReportErrorOpen,
};

case 'user/report-error':
return {
...state,
isReportErrorOpen: false,
};

default:
return state;
}
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/modules/data/components/data-request/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import compact from 'lodash/compact';
import get from 'lodash/get';
import isArray from 'lodash/isArray';
import isEmpty from 'lodash/isEmpty';
import { reportError } from '@src/modules/app/actions';

import ErrorComponent from '../error';

Expand All @@ -30,12 +31,17 @@ function withRequest(Component) {
}

componentDidCatch(error, info) {
const { onError } = this.props;
const { message } = error;

this.setState({
error: {
message: error.message,
message,
info: info.componentStack,
},
});

onError(message);
}

getParams = () => {
Expand Down Expand Up @@ -100,7 +106,9 @@ const mapStateToProps = () => ({});
const composedWithRequest = compose(
connect(
mapStateToProps,
null
{
onError: reportError,
}
),
withRequest
);
Expand Down
37 changes: 25 additions & 12 deletions frontend/src/modules/data/components/error/index.jsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import Button from '@atlaskit/button';
import Button, { ButtonGroup } from '@atlaskit/button';
import ErrorIcon from '@atlaskit/icon/glyph/error';
import { colors } from '@atlaskit/theme';
import reportErrorButton from '@src/modules/app/containers/report-error-button';

import * as styles from './styles.css';

const ReportErrorButton = reportErrorButton(props => (
<Button {...props} appearance="danger">
Report this Error
</Button>
));

function ErrorComponent({ data }) {
// {data.info}

return (
<div className={styles.container}>
<div>
<div>
<ErrorIcon size="xlarge" primaryColor={colors.R500} />
</div>
<h3>Something broke!</h3>
<p>Send the following to a developer to report</p>
<p>
This error has been logged. Please report this error if you would like
to add any additional details.
</p>
<pre style={{ color: colors.R500 }}>{data.message}</pre>
<br />
{window.__REDUX_DEVTOOLS_EXTENSION__ && (
<Button
appearance="danger"
onClick={() => window.__REDUX_DEVTOOLS_EXTENSION__.open()}
>
Show Redux DevTool
</Button>
)}
<h5>Stack trace:</h5>
<pre style={{ borderColor: colors.R500 }}>{data.info}</pre>
<ButtonGroup>
<ReportErrorButton />
{window.__REDUX_DEVTOOLS_EXTENSION__ && (
<Button
appearance="danger"
onClick={() => window.__REDUX_DEVTOOLS_EXTENSION__.open()}
>
Show Redux DevTool
</Button>
)}
</ButtonGroup>
</div>
</div>
);
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/services/middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import ky from 'ky';

const logError = (error, state, action) => {
ky.post('/log', {
json: {
state,
action,
error,
},
});
};

/**
Crash Reporter lifted directly from the Redux docs.
*/
export const crashReporter = store => next => action => {
try {
return next(action);
} catch (err) {
const state = store.getState();
console.error('[ERROR]', err);

logError(err, state, action);

throw err;
}
};

export const errorReporter = store => next => action => {
if (action.error) {
const state = store.getState();

logError(action.type, state, action);
}

return next(action);
};
4 changes: 3 additions & 1 deletion frontend/src/services/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { compose, createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { createLogger } from 'redux-logger';

import { crashReporter, errorReporter } from './middleware';

const devTool = window.devToolsExtension ? window.devToolsExtension() : f => f;

export default function(reducers, sagas) {
const enhancers = [devTool];
const sagaMiddleware = createSagaMiddleware();
const middleware = [sagaMiddleware];
const middleware = [sagaMiddleware, errorReporter, crashReporter];

if (__DEV__) {
middleware.push(createLogger());
Expand Down

0 comments on commit 50a2f61

Please sign in to comment.