Skip to content

Instantly share code, notes, and snippets.

@justjake

justjake/server-preload.js

Last active Jul 15, 2021
Embed
What would you like to do?
Customizing NextJS for error reporting and Datadog APM (dd-trace) integration. See https://jake.tl/notes/2021-04-04-nextjs-preload-hack
// @ts-check
"use strict"
/**
* Set up datadog tracing. This should be called first, so Datadog can hook
* all the other dependencies like `http`.
*/
function setUpDatadogTracing() {
const { tracer: Tracer } = require('dd-trace')
const tracer = Tracer.init({
// Your options here.
runtimeMetrics: true,
logInjection: true,
})
}
/**
* Polyfill DOMParser for react-intl
* Otherwise react-intl spews errors related to formatting
* messages with <xml>in them</xml>
*/
function setUpDOMParser() {
const xmldom = require("xmldom")
global["DOMParser"] = xmldom.DOMParser
}
/**
* Set up logging. Monkey patches a bunch of stuff.
*/
function setUpLogging() {
// pino is a simple JSON logger with Datadog integration.
// By default it logs to STDOUT.
const pino = require('pino')
const logger = pino({
// Your options here.
})
function getLoggingFunction(/** @type {string} */ levelName) {
const baseLogFn = (logger[levelName] || logger.info).bind(logger)
return function patchedLog(/** @type {any[]} */ ...parts) {
/** @type {object | undefined} */
let data = undefined
/** @type {object | undefined} */
let error = undefined
/** @type {object | undefined} */
const nativeError = parts.find(
it =>
(it && it instanceof Error) ||
(it && typeof it === "object" && "name" in it && "message" in it)
)
if (nativeError) {
error = cleanObjectForSerialization(nativeError)
// If you use Sentry, Rollbar, etc, you could capture the error here.
// ErrorThingy.report(nativeError)
}
// If next is trying to log funky stuff, put it into the data object.
if (parts.length > 1) {
data = data || {}
data.parts = parts.map(part => cleanObjectForSerialization(part))
}
const messages =
nativeError && parts.length === 1 ? [nativeError.toString()] : parts
baseLogFn({ data, error, type: levelName }, ...messages)
}
}
// Monkey-patch Next.js logger.
// See https://github.com/atkinchris/next-logger/blob/main/index.js
// See https://github.com/vercel/next.js/blob/canary/packages/next/build/output/log.ts
const nextBuiltInLogger = require("next/dist/build/output/log")
for (const [property, value] of Object.entries(nextBuiltInLogger)) {
if (typeof value !== "function") {
continue
}
nextBuiltInLogger[property] = getLoggingFunction(property)
}
/**
* Monkey-patch global console.log logger. Yes. Sigh.
* @type {Array<keyof typeof console>}
*/
const loggingProperties = ["log", "debug", "info", "warn", "error"]
for (const property of loggingProperties) {
console[property] = getLoggingFunction(property)
}
// Add general error logging.
process.on("unhandledRejection", (error, promise) => {
logger.error(
{
type: "unhandledRejection",
error: cleanObjectForSerialization(error),
data: { promise: cleanObjectForSerialization(promise) },
},
`${error}`
)
})
process.on("uncaughtException", error => {
logger.error(
{ type: "uncaughtException", error: cleanObjectForSerialization(error) },
`${error}`
)
})
}
function cleanObjectForSerialization(value) {
// Clean up or copy `value` so our logger or error reporting system
// can record it.
//
// Because our logger `pino` uses JSON.stringify, we need to do
// the following here:
//
// 1. Remove all cycles. JSON.stringify throws an error when you pass
// a value with cyclical references.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value
// 2. Because JSON.stringify only serializes enumerable properties, we
// need to copy interesting, but non-enumerable properties like
// value.name and value.message for errors:
// JSON.stringify(new Error('nothing serialized')) returns '{}'
//
// Implementing this correctly is beyond the scope of my example.
return value
}
setUpDatadogTracing()
setUpDOMParser()
setUpLogging()
@arihantverma

This comment has been minimized.

Copy link

@arihantverma arihantverma commented Jul 15, 2021

Hi @justjake!

  1. I have general logging question on top of your write up. How do we grab hold of incoming request, for unhandled promise rejections or unhandled errors events, that say happen somewhere in our getServerSideProps code, because of an incoming request? And does it even make sense to log the request data along with the error details ( which are already caught by node events ).
  2. Following on the above query, if it does make sense to add request data with errors, would that have to happen inside getServerSideProps itself? ( by wrapping the whole function in a try catch say ? ). If it doesn't make sense, how do we inject some kind of trace id for logs per request for pino with a particular APM client? ( like this winston enhancer with new relic )

I've never implemented logging in any application before and am trying to wrap my head around what data logged where helps in tracing something having gone wrong. I couldn't find any production code or resources which would help me find some of these answers. If you could direct me to some that you might know that'd be of help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment