diff --git a/lib/failbot.js b/lib/failbot.js
index 0189ff27a9df..46ed67babdac 100644
--- a/lib/failbot.js
+++ b/lib/failbot.js
@@ -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 (!process.env.HAYSTACK_URL) return
+ if (!process.env.HAYSTACK_URL) {
+ return
+ }
const backends = [
new HTTPBackend({
@@ -47,6 +49,7 @@ export async function report(error, metadata) {
app: HAYSTACK_APP,
backends,
})
+
return failbot.report(error, metadata)
}
diff --git a/lib/handle-exceptions.js b/lib/handle-exceptions.js
index a6ce0adc57e7..dda3894c9a21 100644
--- a/lib/handle-exceptions.js
+++ b/lib/handle-exceptions.js
@@ -10,7 +10,7 @@ process.on('uncaughtException', async (err) => {
console.error(err)
try {
- await FailBot.report(err)
+ FailBot.report(err)
} catch (failBotError) {
console.warn('Even sending the uncaughtException error to FailBot failed!')
console.error(failBotError)
@@ -20,7 +20,7 @@ process.on('uncaughtException', async (err) => {
process.on('unhandledRejection', async (err) => {
console.error(err)
try {
- await FailBot.report(err)
+ FailBot.report(err)
} catch (failBotError) {
console.warn('Even sending the unhandledRejection error to FailBot failed!')
console.error(failBotError)
diff --git a/middleware/render-page.js b/middleware/render-page.js
index 6f219eaf2408..8f4a1e054254 100644
--- a/middleware/render-page.js
+++ b/middleware/render-page.js
@@ -1,5 +1,6 @@
import { get } from 'lodash-es'
+import FailBot from '../lib/failbot.js'
import patterns from '../lib/patterns.js'
import getMiniTocItems from '../lib/get-mini-toc-items.js'
import Page from '../lib/page.js'
@@ -40,6 +41,13 @@ async function buildMiniTocItems(req) {
export default async function renderPage(req, res, next) {
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 path = req.pagePath || req.path
browserCacheControl(res)
diff --git a/next.config.js b/next.config.js
index e427ea4c616b..bf3db7e25424 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,6 +1,8 @@
import fs from 'fs'
-import frontmatter from 'gray-matter'
import path from 'path'
+
+import frontmatter from 'gray-matter'
+
const homepage = path.posix.join(process.cwd(), 'content/index.md')
const { data } = frontmatter(fs.readFileSync(homepage, 'utf8'))
const productIds = data.children
diff --git a/pages/500.tsx b/pages/500.tsx
deleted file mode 100644
index 0b75f32263f4..000000000000
--- a/pages/500.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { GenericError } from 'components/GenericError'
-
-export default function Custom500() {
- return
-}
diff --git a/pages/_error.tsx b/pages/_error.tsx
index 2f6fd315f8b3..11ab2864836f 100644
--- a/pages/_error.tsx
+++ b/pages/_error.tsx
@@ -1,10 +1,104 @@
-import { NextPage } from 'next'
-import { GenericError } from 'components/GenericError'
+import type { NextPageContext } from 'next'
-type Props = {}
+import { GenericError } from 'components/GenericError'
-const ErrorPage: NextPage = () => {
+function Error() {
return
}
-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>`.
+ 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