send errors to Failbot in SSR rendering (#30153)
* send errors to failbot in SSR rendering * wip * progress * tidying up * no point awaiting something that doens't return a promise
This commit is contained in:
@@ -33,9 +33,11 @@ async function retryingGot(url, args) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function report(error, metadata) {
|
export function report(error, metadata) {
|
||||||
// If there's no HAYSTACK_URL set, bail early
|
// If there's no HAYSTACK_URL set, bail early
|
||||||
if (!process.env.HAYSTACK_URL) return
|
if (!process.env.HAYSTACK_URL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const backends = [
|
const backends = [
|
||||||
new HTTPBackend({
|
new HTTPBackend({
|
||||||
@@ -47,6 +49,7 @@ export async function report(error, metadata) {
|
|||||||
app: HAYSTACK_APP,
|
app: HAYSTACK_APP,
|
||||||
backends,
|
backends,
|
||||||
})
|
})
|
||||||
|
|
||||||
return failbot.report(error, metadata)
|
return failbot.report(error, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ process.on('uncaughtException', async (err) => {
|
|||||||
|
|
||||||
console.error(err)
|
console.error(err)
|
||||||
try {
|
try {
|
||||||
await FailBot.report(err)
|
FailBot.report(err)
|
||||||
} catch (failBotError) {
|
} catch (failBotError) {
|
||||||
console.warn('Even sending the uncaughtException error to FailBot failed!')
|
console.warn('Even sending the uncaughtException error to FailBot failed!')
|
||||||
console.error(failBotError)
|
console.error(failBotError)
|
||||||
@@ -20,7 +20,7 @@ process.on('uncaughtException', async (err) => {
|
|||||||
process.on('unhandledRejection', async (err) => {
|
process.on('unhandledRejection', async (err) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
try {
|
try {
|
||||||
await FailBot.report(err)
|
FailBot.report(err)
|
||||||
} catch (failBotError) {
|
} catch (failBotError) {
|
||||||
console.warn('Even sending the unhandledRejection error to FailBot failed!')
|
console.warn('Even sending the unhandledRejection error to FailBot failed!')
|
||||||
console.error(failBotError)
|
console.error(failBotError)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { get } from 'lodash-es'
|
import { get } from 'lodash-es'
|
||||||
|
|
||||||
|
import FailBot from '../lib/failbot.js'
|
||||||
import patterns from '../lib/patterns.js'
|
import patterns from '../lib/patterns.js'
|
||||||
import getMiniTocItems from '../lib/get-mini-toc-items.js'
|
import getMiniTocItems from '../lib/get-mini-toc-items.js'
|
||||||
import Page from '../lib/page.js'
|
import Page from '../lib/page.js'
|
||||||
@@ -40,6 +41,13 @@ async function buildMiniTocItems(req) {
|
|||||||
|
|
||||||
export default async function renderPage(req, res, next) {
|
export default async function renderPage(req, res, next) {
|
||||||
const { context } = req
|
const { context } = req
|
||||||
|
|
||||||
|
// This is a contextualizing the request so that when this `req` is
|
||||||
|
// ultimately passed into the `Error.getInitialProps` function,
|
||||||
|
// which NextJS executes at runtime on errors, so that we can
|
||||||
|
// from there send the error to Failbot.
|
||||||
|
req.FailBot = FailBot
|
||||||
|
|
||||||
const { page } = context
|
const { page } = context
|
||||||
const path = req.pagePath || req.path
|
const path = req.pagePath || req.path
|
||||||
browserCacheControl(res)
|
browserCacheControl(res)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import frontmatter from 'gray-matter'
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
|
import frontmatter from 'gray-matter'
|
||||||
|
|
||||||
const homepage = path.posix.join(process.cwd(), 'content/index.md')
|
const homepage = path.posix.join(process.cwd(), 'content/index.md')
|
||||||
const { data } = frontmatter(fs.readFileSync(homepage, 'utf8'))
|
const { data } = frontmatter(fs.readFileSync(homepage, 'utf8'))
|
||||||
const productIds = data.children
|
const productIds = data.children
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { GenericError } from 'components/GenericError'
|
|
||||||
|
|
||||||
export default function Custom500() {
|
|
||||||
return <GenericError />
|
|
||||||
}
|
|
||||||
104
pages/_error.tsx
104
pages/_error.tsx
@@ -1,10 +1,104 @@
|
|||||||
import { NextPage } from 'next'
|
import type { NextPageContext } from 'next'
|
||||||
|
|
||||||
import { GenericError } from 'components/GenericError'
|
import { GenericError } from 'components/GenericError'
|
||||||
|
|
||||||
type Props = {}
|
function Error() {
|
||||||
|
|
||||||
const ErrorPage: NextPage<Props> = () => {
|
|
||||||
return <GenericError />
|
return <GenericError />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ErrorPage
|
Error.getInitialProps = async (ctx: NextPageContext) => {
|
||||||
|
// If this getInitialProps() is called in client-side rendering,
|
||||||
|
// you won't have a `.res` object. It's only present when it's
|
||||||
|
// rendered Node (SSR). That's our clue to know that, we should
|
||||||
|
// send this error to Failbot.
|
||||||
|
// In client-side, it's undefined. In server, it's a ServerResponse object.
|
||||||
|
const { err, req, res } = ctx
|
||||||
|
let statusCode = 500
|
||||||
|
if (res?.statusCode) {
|
||||||
|
statusCode = res.statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'err' will by falsy if it's a 404
|
||||||
|
// But note, at the time of writing this comment, we have a dedicated
|
||||||
|
// `pages/404.tsx` which takes care of 404 messages.
|
||||||
|
if (err && res && req) {
|
||||||
|
// This is a (necessary) hack!
|
||||||
|
// You can't import `../lib/failbot.js` here in this
|
||||||
|
// file because it gets imported by webpack to be used in the
|
||||||
|
// client-side JS bundle. It *could* be solved by overriding
|
||||||
|
// the webpack configuration in our `next.config.js` but this is prone
|
||||||
|
// to be fragile since ignoring code can be hard to get right
|
||||||
|
// and the more we override there, the harder it will become to
|
||||||
|
// upgrade NextJS in the future because of moving parts.
|
||||||
|
// So the solution is to essentially do what the contextualizers
|
||||||
|
// do which mutate the Express request object by attaching
|
||||||
|
// callables to it. This way it's only ever present in SSR executed
|
||||||
|
// code and doesn't need any custom webpack configuration.
|
||||||
|
const expressRequest = req as any
|
||||||
|
const FailBot = expressRequest.FailBot
|
||||||
|
if (FailBot) {
|
||||||
|
try {
|
||||||
|
// An inclusion-list of headers we're OK with sending because
|
||||||
|
// they don't contain an PII.
|
||||||
|
const OK_HEADER_KEYS = ['user-agent', 'referer', 'accept-encoding', 'accept-language']
|
||||||
|
const reported = FailBot.report(err, {
|
||||||
|
path: req.url,
|
||||||
|
request: JSON.stringify(
|
||||||
|
{
|
||||||
|
method: expressRequest.method,
|
||||||
|
query: expressRequest.query,
|
||||||
|
language: expressRequest.language,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
headers: JSON.stringify(
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(req.headers).filter(([k]) => OK_HEADER_KEYS.includes(k))
|
||||||
|
),
|
||||||
|
undefined,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Within FailBot.report() (which is our wrapper for Failbot in
|
||||||
|
// the `@github/failbot` package), it might exit only because
|
||||||
|
// it has no configured backends to send to. I.e. it returns undefined.
|
||||||
|
// Otherwise, it should return `Array<Promise<Response | void>>`.
|
||||||
|
if (!reported) {
|
||||||
|
console.warn(
|
||||||
|
'The FailBot.report() returned undefined which means the error was NOT sent to Failbot.'
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
Array.isArray(reported) &&
|
||||||
|
reported.length &&
|
||||||
|
reported.every((thing) => thing instanceof Promise)
|
||||||
|
) {
|
||||||
|
// Make sure we await the promises even though we don't care
|
||||||
|
// about the results. This makes the code cleaner rather than
|
||||||
|
// letting the eventloop take care of it which could result
|
||||||
|
// in cryptic error messages if the await does fail for some
|
||||||
|
// reason.
|
||||||
|
try {
|
||||||
|
await Promise.all(reported)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Unable to await reported FailBot errors', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// This does not necessarily mean FailBot failed to send. It's
|
||||||
|
// most likely that we failed to *send to* FailBot before it
|
||||||
|
// even has a chance to use the network. This is because
|
||||||
|
// `FailBot.report` returns an array of Promises which themselves
|
||||||
|
// could go wrong, but that's a story for another try/catch.
|
||||||
|
// Basically, this catch it just to avoid other errors that
|
||||||
|
// might prevent the pretty error page to render at all.
|
||||||
|
console.warn('Failed to send error to FailBot.', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { statusCode, message: err?.message }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Error
|
||||||
|
|||||||
Reference in New Issue
Block a user