Closed acanimal closed 2 years ago
For logging errors in the client side we have been doing the following: https://gist.github.com/jgautheron/044b88307d934d486f59ae87c5a5a5a0
It basically sends errors to the server, that are in the end printed to stdout
and caught by our Docker
logging driver.
We have successfully caught SSR errors with react-guard
with no need to override core Next files.
I'm also having this problem. I wonder: would simply returning a rejected promise from next's handleRequest
be sufficient? That is, changing the code here to be:
handleRequest (req, res, parsedUrl) {
// .....snip....
return this.run(req, res, parsedUrl)
.catch((err) => {
if (!this.quiet) console.error(err)
res.statusCode = 500
res.end(STATUS_CODES[500])
// rethrow error to create new, rejected promise
throw err;
})
}
Then, in user code:
const app = nextJs({ dev: process.env.NODE_ENV !== 'production' });
const nextJsHandler = app.getRequestHandler();
const expressApp = express();
app.prepare().then(() => {
// invoke express middlewares
// ...
// time to run next
expressApp.use(function(req, res, next) {
nextJsHandler(req, res).catch(e => {
// use rejected promise to forward error to next express middleware
next(e)
})
});
// Use standard express error middlewares to handle the error from next
// this makes it easy to log to sentry etc.
})
@arunoda @rauchg Do you think the change I proposed immediately above would work? If so, happy to submit a PR
Agree to re-throw an error so we can play around with it.
Also need to re-throw at renderToHTML
too...
I am also in the situation where we want to sent errors, both on server and client side, to a service similar to Sentry.
I believe that the most valuable feature of such services/tools is to report issues that are most unexpected, which in my experience are uncaught errors in the wild (ie. client-side). Unfortunately, as previously stressed out in related issue #2334, Next.js’ client-side handlers keep these errors to themselves, with no possible way to pass them to Sentry of such other tool.
What has bitten us particularly hard is this: a properly server-side rendered page is re-rendered as an error page if an uncaught exception occurs before React rendering on the client-side. This can be seen as either a great feature, but also a frustrating developer experience, as it essentially ruins the benefits of serving an already rendered document, on a surprisingly portion of clients.
Here is a sample page that illustrates the problem:
import React from 'react';
// Works well in Node 8, but crashes in Chrome<56, Firefox<48, Edge<15, Safari<10, any IE…
const whoops = 'Phew, I made it to the client-side…'.padEnd(80);
export default () => <pre>{whoops}</pre>;
The above code can be perfectly server-side-rendered and delivered to the client, only to become a Flash Of Unwanted Content before the whole page is replaced on the client-side by the dreaded ”An unexpected error has occurred” message on most browsers, without any possible way to report the error to Sentry (or any other service/tool).
This ”error swallowing” also prevents any leverage of the standard onerror handler, which most client-side error reporting tools hook onto (or the more modern, but not widespread, onunhandledrejection, which may be more suitable given the async nature of client-side code).
As far as I can tell, this type of pre-React client-side error is swallowed in the try
/catch
block in Next.js’ client/index.js where the Component
about to be rendered is re-assigned to the value of ErrorComponent
(currently lines 67-72).
Dear Next.js authors and maintainers, for the sake of control of what is rendered, what would you think would be acceptable/possible among the following ideas:
catch
block in client/index.js for handling this kind of error?introduce a hook in that catch block in client/index.js for handling this kind of error? transmit the error to an onerror/onunhandledrejection handler, if any is detected?
This is something we've talked about internally. And we'll be addressing soon.
I'm using Sentry.io to report errors and the solution we apply is:
1- Configure raven-node on server side
2- Configure ravenjs on client side (we made that on _document
.
With this two steps we catch any unhandled exceptions both on client and server.
3- Create an _error
page. Any error produced once nextjs handles the request (no matter if client or server side) that page is rendered. In the getInitialProps
method of the _error page we report the error to sentry.
The way to decide how to load if raven-node or ravenjs is solved importing dynamically depending if we are in client const Raven = require('raven-js');
or server side const Raven = require('raven');
.
Note we have configured webpack to not bundle the raven
module (the server side one) updating next.config.js
with:
const webpack = require('webpack');
module.exports = {
// Do not show the X-Powered-By header in the responses
poweredByHeader: false,
webpack: (config) => {
config.plugins.push(new webpack.IgnorePlugin(/^raven$/));
return config;
},
};
@acanimal
2- Configure ravenjs on client side (we made that on _document.
Can you show me how did you configured raven-js on your _document.js
? It's not working for me, when any error occur nothing happens on sentry.
Do I need to send the all the errors manually at _error.js
page to sentry?
// _document constructor
constructor(props) {
super(props);
Raven
.config('...')
.install();
}
Next.js still outputs errors to console.error
on the server as long as you don't set it to quiet.
With Sentry, you can enable autoBreadcrumbs
to capture this output, and then capture your own message manually. The title will be less descriptive, but it will still contain the full stack trace.
Example implementation:
const express = require('express');
const nextjs = require('next');
const Raven = require('raven');
const dev = process.env.NODE_ENV !== 'production';
// Must configure Raven before doing anything else with it
if (!dev) {
Raven.config('__DSN__', {
autoBreadcrumbs: true,
captureUnhandledRejections: true,
}).install();
}
const app = nextjs({ dev });
const handle = app.getRequestHandler();
const captureMessage = (req, res) => () => {
if (res.statusCode > 200) {
Raven.captureMessage(`Next.js Server Side Error: ${res.statusCode}`, {
req,
res,
});
}
};
app
.prepare()
.then(() => {
const server = express();
if (!dev) {
server.use((req, res, next) => {
res.on('close', captureMessage(req, res));
res.on('finish', captureMessage(req, res));
next();
});
}
[...]
server.get('/', (req, res) => {
return app.render(req, res, '/home', req.query)
})
server.get('*', (req, res) => {
return handle(req, res)
})
server.listen('3000', (err) => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`)
})
})
.catch(ex => {
console.error(ex.stack);
process.exit(1);
});
This is a very contrived example adapted from our actual code. I haven't tested it in this form. Let me know if it breaks.
Of course, it would still be best if Next.js could pass those errors around to Express, so that we can use the Sentry/Express integration out of the box.
@tusgavomelo Sorry, for the late reply.
We have update. In our app we have a helper file with a method responsible to get a Raven instance taking into account if we are on client or server side.
let clientInstance;
let serverInstance;
const getRavenInstance = (key, config) => {
const clientSide = typeof window !== 'undefined';
if (clientSide) {
if (!clientInstance) {
const Raven = require('raven-js'); // eslint-disable-line global-require
Raven.config(key, config).install();
clientInstance = Raven;
}
return clientInstance;
}
if (!serverInstance) {
// NOTE: raven (for node) is not bundled by webpack (see rules in next.config.js).
const RavenNode = require('raven'); // eslint-disable-line global-require
RavenNode.config(key, config).install();
serverInstance = RavenNode;
}
return serverInstance;
};
This code runs both server side (when a request arrives) and client side. What we have done is configure webpack (next.config.js
file) to avoid bundle the raven
package.
@acanimal can you maybe provide a working example ? It seems I am not getting the full stack trace ? or maybe can you post your _error.js
?
I am doing something like RavenInstance.captureException(err)
in my _error.js
, but I do not get to see the type of errors that occured as well as where?
export default class Error extends React.Component {
static getInitialProps({ res, err }) {
const RavenInstance = getRavenInstance('__SENTRY__')
if (!(err instanceof Error)) {
err = new Error(err && err.message)
}
RavenInstance.captureException(err)
// const statusCode = res ? res.statusCode : err ? err.statusCode : null;
return { }
}
render() {
return (
<div>
<p>An error occurred on server</p>
</div>
)
}
}
This seems like the place to ask for custom logger support since quiet
must be set to false
to prevent next clearing screen on module rebuild and thus purging errors from view, yet the only workable hack here requires setting quiet
to false.
The errors details are also available on the stderr stream.
process.stderr.write = error => yourErrorLog(error);
Gotta love unortodox solutions
function installErrorHandler(app) {
const _renderErrorToHTML = app.renderErrorToHTML.bind(app)
const errorHandler = rollbar.errorHandler()
app.renderErrorToHTML = (err, req, res, pathname, query) => {
if (err) {
errorHandler(err, req, res, () => {})
}
return _renderErrorToHTML(err, req, res, pathname, query)
}
return app
}
// ¯\_(ツ)_/¯
Compact version of how to get correct Raven for node and browser w/o custom webpack config. Inspired by @acanimal comment
// package.json
"browser": {
"raven": "raven-js"
}
// getRaven.js
const Raven = require('raven')
if (process.env.NODE_ENV === 'production') {
Raven.config('YOUR_SENTRY_DSN').install()
}
module.exports = Raven
a quick recap for anyone who investigating on this problem.
// pages/_error.js
import Raven from 'raven';
...
static async getInitialProps({ store, err, isServer }) {
if (isServer && err) {
// https://github.com/zeit/next.js/issues/1852
// eslint-disable-next-line global-require
const Raven = require('raven');
Raven.captureException(err);
}
...
// next.config.js
config.plugins.push(new webpack.IgnorePlugin(/^raven$/));
thanks to @acanimal 's comment.
Install both raven
(ignore with webpack as suggested in previous comments) and raven-js
, then create a helper to instantiate isomorphic Raven, e.g. lib/raven.js
import Raven from 'raven-js';
// https://gist.github.com/impressiver/5092952
const clientIgnores = {
ignoreErrors: [
'top.GLOBALS',
'originalCreateNotification',
'canvas.contentDocument',
'MyApp_RemoveAllHighlights',
'http://tt.epicplay.com',
"Can't find variable: ZiteReader",
'jigsaw is not defined',
'ComboSearch is not defined',
'http://loading.retry.widdit.com/',
'atomicFindClose',
'fb_xd_fragment',
'bmi_SafeAddOnload',
'EBCallBackMessageReceived',
'conduitPage',
'Script error.',
],
ignoreUrls: [
// Facebook flakiness
/graph\.facebook\.com/i,
// Facebook blocked
/connect\.facebook\.net\/en_US\/all\.js/i,
// Woopra flakiness
/eatdifferent\.com\.woopra-ns\.com/i,
/static\.woopra\.com\/js\/woopra\.js/i,
// Chrome extensions
/extensions\//i,
/^chrome:\/\//i,
// Other plugins
/127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
/webappstoolbarba\.texthelp\.com\//i,
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
],
};
const options = {
autoBreadcrumbs: true,
captureUnhandledRejections: true,
};
let IsomorphicRaven = null;
if (process.browser === true) {
IsomorphicRaven = Raven;
IsomorphicRaven.config(SENTRY_PUBLIC_DSN, {
...clientIgnores,
...options,
}).install();
} else {
// https://arunoda.me/blog/ssr-and-server-only-modules
IsomorphicRaven = eval("require('raven')");
IsomorphicRaven.config(
SENTRY_DSN,
options,
).install();
}
export default IsomorphicRaven;
Then you can use it in your pages/_error.js
and it will work both on server and client side.
import NextError from 'next/error';
import IsomorphicRaven from 'lib/raven';
class MyError extends NextError {
static getInitialProps = async (context) => {
if (context.err) {
IsomorphicRaven.captureException(context.err);
}
const errorInitialProps = await NextError.getInitialProps(context);
return errorInitialProps;
};
}
export default MyError;
Here is my PR for Rollbar sourcemap wepback plugin https://github.com/thredup/rollbar-sourcemap-webpack-plugin/pull/56 with Next.js support :)
@tusgavomelo , Can you elaborate on how to make use of the error present on the stream ? "process.stderr.write = error => yourErrorLog(error);"
Where should we write this line of code, for it to log the error to Node console ?
That is only Client side.
Rollbar.error('some error')
@teekey99 Do you have similar solution for @sentry/browser
? Maybe update with-sentry example?
@sheerun I've been using raven
and raven-js
so far. I am aware that those will probably become deprecated as all new features are now added to @sentry/node
and @sentry/browser
. The thing is I haven't used these new libraries on my projects yet, but I'll try to look into it. If I have a working example I'll get back with it.
The with-sentry example was recently updated.
@timneutkens I see but it doesn't support server-side errors as Sentry is initialized inside App where it's too late to catch server errors. Proper solution would probably use @sentry/node
somewhere
@sheerun I'm hitting the same issue. Using @sentry/node
isn't trivial. The readme for this Sentry example suggests using a custom server, which we already have in the application I'm working on. To capture exceptions in our custom Express.js server, you need to insert a Sentry error handler middleware error as the first error handling middleware. If you insert Sentry's error handler straight after the Next handler, the error has already been swallowed by that point and Sentry doesn't see it.
I've been using @sentry/browser
in the getInitialProps
of _error.js
and it seems to be working both client and server-side. I don't know if @sentry/browser
is supposed to have server-side support, but I am getting events to Sentry.
Though I am also calling Sentry.init()
via @sentry/node
in a custom Express server's entry file, so perhaps that is setting some global state that the SSR is using.
Here's a gist of the setup that I'm using: https://gist.github.com/mcdougal/7bf001417c3dc4b579da224b12776691
Interesting!
There's definitely some kind of global state going on here (which is a bit scary, and probably brittle and undesirable). Applying your changes in _error.js
:
ReferenceError: XMLHttpRequest is not defined
error in the server logsError: Sentry syntheticException
recorded in SentryIt would be good to understand what the official solution is. Next's docs seem to recommend using a custom <App>
component now, and that's what the "with Sentry" example does, but this only works for the client side.
Error reporting for sure won't happen until first time App is rendered. Next can fail way before e.g. when rendering Page. Maybe by chance it happens to work in some cases after app has rendered on server side first time, but for sure it's not a full solution.
The with-sentry example does not work for me either @timneutkens. Running the project and testing it with my actual DSN gives out a 400 response and nothing gets to sentry (version 7 of the api).
{"error":"Bad data reconstructing object (JSONDecodeError, Expecting value: line 1 column 1 (char 0))"}
The @mcdougal did not work for me. It was already not quite great to have a global server-side setup that somehow bubbles up to the client, but even with that hack I would get sentry 301 responses.
I dont see how my sentry self-hosted setup could have a misconfiguration, as there aren't many options and Its running on multiple projects.
I'm using @sentry/browser
in the same way that it is recommended in the with-sentry example, and it seems to be sending errors both from the server and the client to my sentry. I don't have any special config, only the same code as the example.
@Jauny Are you sure it is sending from the server? How have you tested that? The readme of the example even seems to suggest that it won't work on the server.
@timrogers yes indeed my bad, I assumed a tested error was coming from the server but actually was triggered from the client :(
It seems that current example with-sentry doesn't catch errors thrown in getInitialProps, just ones in render tree of application.. In my case most of the errors are from getInitialProps.
@sheerun Can you try the mcdougal workaround above? It isn't perfect (strange synthetic sentry errors) but I am under the impression it gets all errors and would like to know if that is not true. If it is true, the with-sentry example probably needs to be updated with that until Next.js can advise on how to do it better (ideally making it so the server.js sentry error handler isn't skipped?).
It seems it kinda works with two major issues:
Actually after further testing getInitialProps of custom Error for some reason is not even firing in production when error happens for example inside custom _app's getInitialProps..
Yeah, I've definitely been getting some weird behavior after running with my attempt for a few days. Seems like the main problems we're facing are:
@sentry/browser
for CSR and @sentry/node
for SSRI've been thinking of trying to use lazy imports and global state to solve #1
, something like
const sentryInitialized = false;
# Inside some trigger point
const Sentry = ssr ? eval(`require('@sentry/node')`) : eval(`require('@sentry/browser')`);
if (!sentryInitialized) {
Sentry.init({dsn: SENTRY_DSN});
}
Sentry.captureException(err);
Maybe Next's dynamic imports could be used, but I'm not that familiar with them yet. I'm also not sure the repercussions of calling require
every time the error handling code gets triggered. I'll update if/when I give this a try.
As far as problem #2
, seems like the possibilities are:
_app.componentDidCatch
, which doesn't fire for SSR_error.getInitialProps
, which has a multitude of problemsMy gut feeling is that there’ll be a way to do this on the server by injecting Sentry’s error handler before Next’s, but I haven’t given it a go yet.
At the moment, Next doesn’t provide any hooks to help you do that. If we found that this works, we could add them 👌
There’s a risk that this won’t work because Next catches too early, though.
@mcdougal Ah &#!% I was so happy when your workaround might have been good enough to check off the sentry client/server coverage required to get us into production.
Pardon my complete ignorance but do we just need next to allow us to disable its error handler conditionally so that the nodejs sentry error handler becomes the first? Some flag in next.config.js called "disableErrorHandler"?
@Enalmada I don’t think you’d want to disable the Next error handler because it’ll be that which renders a nice error page. You just want to insert other middleware before it. I think that will work, but I need to try it.
Even with that fixed, I still don’t feel like client-side error handling works as well as I’d hope :(
This whole issue is a shame and it really is a blocker to safely running Next in production.
FYI I import everywhere @sentry/node
and put following into next.config.js:
if (!isServer) {
config.resolve.alias['@sentry/node$'] = '@sentry/browser'
}
which can be better than eval
of @mcdougal
Here are my extra notes about custom _app component and error handling:
_app.js
is used for ALL pages, including _error.js
or pages like 404 so you really want to be sure no error is thrown when ctx.err
is passed to it.._error.js
to be called (call it even if ctx.err
is present)class MyApp extends App {
static async getInitialProps (appContext) {
const { Component, ctx } = appContext
if (ctx.err) {
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { error: true, pageProps }
}
// here code that can throw an error, and then:
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps }
}
render() {
if (this.props.error) return super.render()
// rest of code that can throw an error
}
}
So far I find properly setting up error reporting in next.js a very fragile procedure :(
thanks @sheerun that looks like a good stop towards the right direction. I do agree that error handling in next is not optimal right now, it'll be great to see some extensible module/middleware added so we can add error handling on it etc.
The lack of isomorphic libraries such as sentry is also making things complicated, because that means we can't simple import either library in our components, we need to do it dynamically at runtime to always check if the error is raised server or browser side.
Is there an update to this issue? What i tried so far is the following: I moved all our tracking code into the _app.js
constructor(args: any) {
super(args)
Sentry.init({
dsn: 'blah',
environment: 'local',
})
Sentry.configureScope(scope => {
scope.setTag('errorOrigin', isServer ? 'SSR' : 'Client')
})
}
static async getInitialProps({ Component, router, ctx }: any) {
let pageProps = {}
try {
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
} catch (error) {
// console.log('we caught an error')
console.log(error)
Sentry.captureException(error)
throw error
}
return { pageProps }
}
coupled with the next.config.js addition from @sheerun and initializing sentry in the server.js too if (!isServer) { config.resolve.alias['@sentry/node$'] = '@sentry/browser' }
this seems to track all errors on the client side, but on the server side it only seems to track the first error that happens after a restart of the server. Later errors on the server are not tracked though. With this approach i do not have any SyntheticErrors in the log, but only real errors.
Still this feels quite hacky to me and since the server side tracking is only working the first time it is still non-usable.
I also added this part from the with-Sentry example
componentDidCatch(error: any, errorInfo: any) {
// if (process.env.FIAAS_NAMESPACE !== undefined) {
Sentry.configureScope(scope => {
Object.keys(errorInfo).forEach(key => {
scope.setExtra(key, errorInfo[key])
})
})
Sentry.captureException(error)
console.log('componentDidCatch')
// This is needed to render errors correctly in development / production
super.componentDidCatch(error, errorInfo)
// }
}
but i am not totally sure if this is needed
In my case with works without issues. also you shuld not init sentry in _app.js constructor but outside of this class entirely
On Wed, Nov 21, 2018 at 2:53 PM abraxxas notifications@github.com wrote:
Is there an update to this issue? What i tried so far is the following: I moved all our tracking code into the _app.js
` constructor(args: any) { super(args) Sentry.init({ dsn: 'blah', environment: 'local', }) Sentry.configureScope(scope => { scope.setTag('errorOrigin', isServer ? 'SSR' : 'Client') }) }
static async getInitialProps({ Component, router, ctx }: any) { let pageProps = {}
try { if (Component.getInitialProps) { pageProps = await Component.getInitialProps(ctx) } } catch (error) { // console.log('we caught an error') console.log(error) Sentry.captureException(error) throw error } return { pageProps }
}
`
coupled with the next.config.js addition from @sheerun https://github.com/sheerun and initializing sentry in the server.js too if (!isServer) { config.resolve.alias['@sentry/node$'] = '@sentry/browser' } this seems to track all errors on the client side, but on the server side it only seems to track the first error that happens after a restart of the server. Later errors on the server are not tracked though. With this approach i do not have any SyntheticErrors in the log, but only real errors.
Still this feels quite hacky to me and since the server side tracking is only working the first time it is still non-usable.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/zeit/next.js/issues/1852#issuecomment-440668980, or mute the thread https://github.com/notifications/unsubscribe-auth/AAR2DeIhoOj6PdWRA2VqiEZyrO5Jui8vks5uxVrHgaJpZM4NOQlp .
I tried moving it out already and still the same behaviour. @sheerun could you maybe post a minimal gist of your setup? I tried setting it up with the snippets you provided and i just can't get it to work. The whole thing seems overly complicated for what i expected to be a rather simple task :( Are you also initializing sentry on the server or only in _app.js outside the class?
I'd update official sentry example but I'm afraid it'll be rejected as "too complex" I can try to post something though when I'll find time..
@sheerun Even an attempt to update to the official example would be of great value. I feel it would get merged if it really is the minimum complexity necessary to get sentry ssr working without SyntheticErrors or only recording the first server error that happens. Then we can go from there to figure out ways of making it better or pushing for nextjs core improvements or sentry isomorphic support.
Here it goes.. https://github.com/zeit/next.js/pull/5727
So now that we have a necessarily complex working example, what are the next steps to improve the situation:
Hi, I'm in the situation where we want to sent errors, both on server and client side, to Sentry tool.
Our app uses Express as a custom server. Basically we create an express app, apply some middlewares but delegate all the real job to the next.js handle:
With this approach next.js takes control over the rendering process and any error is catch by next.js and the only way I have to process it is overriding the
_error.js
page file.Within that
_error.js
file I need a universal way to report errors to Sentry. Currently there are two libraries (raven
for node andraven-js
por javascript). The proble is I can't import both of them becauseraven
works for SSR but fails when webpack builds the bundle, and alsoraven-js
fails due XMLHTTPRequest dependency too.Is there a way I can be notified for next.js error on server side?