Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Next.js server instrumentation #2959

Merged
merged 70 commits into from
Oct 28, 2022
Merged
Changes from 1 commit
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
27b3fb2
feat: Next.js instrumentation
trentm Sep 29, 2022
e0efb1c
fixes: 'error.transaction.*' capture; correct trans.name when error h…
trentm Sep 29, 2022
4e12451
fix: 'make check'
trentm Sep 29, 2022
a2f9b4e
skip dependency-check in the sample a-nextjs-app because it borks on …
trentm Sep 29, 2022
3171814
typo in comment
trentm Oct 4, 2022
146511a
do the config test pennance for new instrumentation modules
trentm Oct 4, 2022
5452607
get routes going with a dev server. now need to move back to instr on…
trentm Oct 5, 2022
67bad71
flushing on server shutdown; DevServer instrumentation; most of the t…
trentm Oct 7, 2022
ce06f35
drop this unused instrumentation module
trentm Oct 7, 2022
dc04846
fix config.test.js
trentm Oct 11, 2022
ff5dd74
more test cases, fixing some edge cases with error capture
trentm Oct 12, 2022
4c739fa
use relative require for elastic-apm-node rather than a relative dep;…
trentm Oct 12, 2022
352fcb6
fix make check
trentm Oct 12, 2022
c25c3fe
update package-lock for the recently dropped relative agent dep
trentm Oct 12, 2022
e51919f
drop the slow to install/build 'sharp' suggested for Next.js prod (ht…
trentm Oct 12, 2022
4ffd5ef
strip down tests that are run for quicker dev; solve the test hang
trentm Oct 13, 2022
b2f31b7
nope to Next.js instr with contextManager=patch config, skip those
trentm Oct 13, 2022
ade6c03
restore full testing
trentm Oct 13, 2022
5e2cc59
some debugging for tests failing in CI
trentm Oct 13, 2022
c556305
fix tests for v14.0 (skip); and hopefully for Windows CI
trentm Oct 13, 2022
18bbd38
test fixes: skip Windows; give 20s for Next.js server to come up on s…
trentm Oct 13, 2022
ce94960
give shell=true on win32 a try for Next.js testing on win32
trentm Oct 13, 2022
382be82
more correct fix for node v18 tests: the issue was ipv4 vs ipv6 disag…
trentm Oct 13, 2022
0e04751
moar trying for Windows tests to get spawn to work
trentm Oct 13, 2022
95c3ad1
nope to Windows testing now, thanks cmd.exe; better dynamic rewrite t…
trentm Oct 13, 2022
0a7f70c
attempt to avoid invalid XML errors/warnings from Jenkins looking at …
trentm Oct 13, 2022
dd55843
fix the recently added stdout/stderr filtering (Buffer -> string)
trentm Oct 13, 2022
07534e5
add TAV testing of Next.js
trentm Oct 13, 2022
7af0031
needed for tav runs in Jenkins
trentm Oct 14, 2022
3a7dc47
Merge branch 'main' into trentm/feat-nextjs
trentm Oct 14, 2022
5c64f03
test another error case: a throw in getServerSideProps
trentm Oct 14, 2022
be1a746
test the /_next/data/... route -- the more important of the various i…
trentm Oct 14, 2022
ed075fb
feat: skip stack frame lines of context for minimized files
trentm Oct 14, 2022
1739501
set metadata.service.framework.*
trentm Oct 14, 2022
9f178ea
move nextPj down after some early test file exits (for when testing w…
trentm Oct 14, 2022
f56988a
move tests to test/instrumentation/modules/next/... where it should be
trentm Oct 14, 2022
0629011
get the next.js version *after* npm ci, else the package.json isn't t…
trentm Oct 14, 2022
f04b509
get dependabot updates for a-nextjs-app test app
trentm Oct 14, 2022
8bb630f
start a Next.js + APM example: this is the state after 'npx create-ne…
trentm Oct 17, 2022
4186e29
steps to add APM support to a Next.js app
trentm Oct 17, 2022
4b9d8c7
first crack at Next.js docs
trentm Oct 18, 2022
1d90b9b
Merge branch 'main' into trentm/feat-nextjs
trentm Oct 18, 2022
b47fb16
add a screenshot
trentm Oct 18, 2022
9d14567
apmsetup.js -> elastic-apm-node/start-next.js; drop the .env file han…
trentm Oct 18, 2022
10ff2d5
fix lint
trentm Oct 18, 2022
f267e38
good for now for docs
trentm Oct 18, 2022
e0c7ac0
get test Next.js app to use start-next.js starter module
trentm Oct 18, 2022
73f5816
missed part of converting the test Next.js app over the using 'start-…
trentm Oct 18, 2022
ac78bf4
next instr docs; clearing out XXXs
trentm Oct 19, 2022
dfecd43
clearning more XXXs
trentm Oct 19, 2022
a7ddf3b
working through XXXs
trentm Oct 19, 2022
ca74a37
cache the wrapping of API endpoint handler modules (i.e. pages/api/*.…
trentm Oct 19, 2022
dac6583
changelog entry; some other doc/comment edits
trentm Oct 20, 2022
ada0a45
Merge branch 'main' into trentm/feat-nextjs
trentm Oct 24, 2022
1729148
Update docs/nextjs.asciidoc
trentm Oct 26, 2022
e621034
Update docs/nextjs.asciidoc
trentm Oct 26, 2022
250b73f
Update docs/nextjs.asciidoc
trentm Oct 26, 2022
1ebd48e
Update docs/nextjs.asciidoc
trentm Oct 26, 2022
931e396
Update docs/nextjs.asciidoc
trentm Oct 26, 2022
b601b22
Update docs/nextjs.asciidoc
trentm Oct 26, 2022
64ca18b
Update examples/nextjs/README.md
trentm Oct 26, 2022
7f7a542
Update examples/nextjs/README.md
trentm Oct 26, 2022
3e2d15b
doc updates (mainly using preview:[]) from Brandon's review
trentm Oct 26, 2022
5ae3a2f
support Next.js 13 and fixes for versions of next before 12.3.1
trentm Oct 27, 2022
33a6510
fix lint
trentm Oct 27, 2022
870c5bb
Merge branch 'main' into trentm/feat-nextjs
trentm Oct 27, 2022
94586a9
changelog tweak after merge
trentm Oct 27, 2022
15c2e9c
test fix: only test next instr with node >=14.6.0 (the min support wi…
trentm Oct 27, 2022
abed53d
clear out last XXXs that were there while this was still on a feature…
trentm Oct 28, 2022
58990ea
a couple final doc/comment edits
trentm Oct 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fixes: 'error.transaction.*' capture; correct trans.name when error h…
…andling

- Set trans.name before a possible captureError handling a route. This
  is because `captureError(err)` will set `error.transaction.*` fields.
  We want the `trans.name` to be set by then.
- Ensure that when an *error* page component is being loaded, it isn't
  used for the transaction name.
trentm committed Sep 29, 2022
commit e0efb1c016345d84302ab7cb1ca2cb80caf305e3
159 changes: 91 additions & 68 deletions lib/instrumentation/modules/next/dist/server/next-server.js
Original file line number Diff line number Diff line change
@@ -8,30 +8,40 @@

// The main (but not only) module for instrumenting the Next.js server.
//
// XXX server hierarchy, mainly about the "prod" server, but mostly should
// work for the dev server `npm run dev`.
//
// Some notes on how the instrumentation works.
//
// - XXX NextNodeServer.handleRequest
// - *API* routes ("pages/api/...") in a Next.js app are handled differently
// from other pages. The `catchAllRoute` calls `handleApiRequest`, which
// resolves the URL path to a possibly dynamic route name (e.g.
// `/api/widgets/[id]`, we instrument `ensureApiPage` to get that resolve
// route name), loads the webpack-compiled user module for that route, and
// calls `apiResolver` in "api-utils/node.ts" to execute. We instrument that
// `apiResolve()` function to capture any errors in the user's handler.
// - For other routes ("pages/..."), XXX
// Some notes on how the Next.js node server and the instrumentation works.
//
// - There are a number of ways to deploy (https://nextjs.org/docs/deployment)
// a Next.js app. This instrumentation works with "Self-Hosting", and using
// Next.js's built-in server (`class NextNodeServer`). This is the server
// that is used for `next build && next start` and a subclass of that server
// for `next dev`.
// - The Next.js server is a vanilla Node.js `http.createServer` using
// `NextNodeServer.handleRequest` as the request handler, so every request
// to the server is a call to that method.
// - Routes are defined by files under "pages/". An incoming request path is
// resolved a built-in Next.js route handler or one of those pages -- loaded
// by `NextNodeServer.findPageComponents`.
// - An error in rendering a page results in `renderErrorToResponse(err)` being
// called to handle that error. (Limitation: There are some edge cases where
// this method is not used to handle an exception. This instrumentation isn't
// capturing those.)
// - *API* routes ("pages/api/...") are handled differently from other pages.
// The `catchAllRoute` route handler calls `handleApiRequest`, which resolves
// the URL path to a possibly dynamic route name (e.g. `/api/widgets/[id]`,
// we instrument `ensureApiPage` to get that resolve route name), loads the
// webpack-compiled user module for that route, and calls `apiResolver` in
// "api-utils/node.ts" to execute. We instrument that `apiResolve()` function
// to capture any errors in the user's handler.
// - There is open discussion here for other ways to support error capture
// for Next.js: https://github.com/vercel/next.js/discussions/32230

const semver = require('semver')

const shimmer = require('../../../../shimmer')

const kRouteName = Symbol('nextJsRouteName')
const kPossibleApiRouteName = Symbol('nextJsPossibleApiRouteName')
const kInErrorHandling = Symbol('nextJsInErrorHandling')
// const kRouteName = Symbol('nextJsRouteName')
// const kPossibleApiRouteName = Symbol('nextJsPossibleApiRouteName')

module.exports = function (mod, agent, { version, enabled }) {
console.log('XXX enabled: ', enabled)
@@ -48,70 +58,73 @@ module.exports = function (mod, agent, { version, enabled }) {
const log = agent.logger

const NextNodeServer = mod.default
// XXX might still use generateRoutes to wrap route.fn functions to capture
// the name of a route for the internal `_next`-y routes
shimmer.wrap(NextNodeServer.prototype, 'generateRoutes', wrapGenerateRoutes)
shimmer.wrap(NextNodeServer.prototype, 'handleRequest', wrapHandleRequest)
shimmer.wrap(NextNodeServer.prototype, 'handleApiRequest', wrapHandleApiRequest)
// shimmer.wrap(NextNodeServer.prototype, 'handleRequest', wrapHandleRequest)
// shimmer.wrap(NextNodeServer.prototype, 'handleApiRequest', wrapHandleApiRequest)
shimmer.wrap(NextNodeServer.prototype, 'ensureApiPage', wrapEnsureApiPage)
shimmer.wrap(NextNodeServer.prototype, 'findPageComponents', wrapFindPageComponents)
shimmer.wrap(NextNodeServer.prototype, 'renderErrorToResponse', wrapRenderErrorToResponse)

/*
XXX
- might still use generateRoutes to wrap route.fn functions to capture
the name of a route for the internal `_next`-y routes

*/

return mod

function wrapGenerateRoutes (orig) {
return function wrappedGenerateRoutes () {
const routes = orig.apply(this, arguments)
console.log('XXX wrappedGenerateRoutes')
// console.log('XXX routes: ', routes)
return routes
}
}

function wrapHandleRequest (orig) {
return function wrappedHandleRequest (req, _res, parsedUrl) {
console.log('\nXXX wrappedHandleRequest(req "%s %s", res, parsedUrl=%s)', req.method, req.url, parsedUrl)
const promise = orig.apply(this, arguments)
promise.then(
() => {
const trans = ins.currTransaction()
if (trans && trans[kRouteName]) {
trans.setDefaultName(`${req.method} ${trans[kRouteName]}`)
}
}
)
return promise
}
}
// function wrapHandleRequest (orig) {
// return function wrappedHandleRequest (req, _res, parsedUrl) {
// console.log('\nXXX wrappedHandleRequest(req "%s %s", res, parsedUrl=%s)', req.method, req.url, parsedUrl)
// const promise = orig.apply(this, arguments)
// promise.then(
// () => {
// const trans = ins.currTransaction()
// if (trans && trans[kRouteName]) {
// trans.setDefaultName(`${req.method} ${trans[kRouteName]}`)
// console.log('XXX setDefaultName: ', trans.name)
// }
// }
// )
// return promise
// }
// }

function wrapHandleApiRequest (orig) {
return function wrappedHandleApiRequest () {
const promise = orig.apply(this, arguments)
promise.then(
handled => {
if (handled) {
// The API request was handled, therefore the route name found
// in the wrapped `ensureApiPage` is the route name.
const trans = ins.currTransaction()
if (trans && trans[kPossibleApiRouteName]) {
trans[kRouteName] = trans[kPossibleApiRouteName]
}
}
}
)
return promise
}
}
// function wrapHandleApiRequest (orig) {
// return function wrappedHandleApiRequest () {
// const promise = orig.apply(this, arguments)
// promise.then(
// handled => {
// if (handled) {
// console.log('XXX handled: ', handled)
// // The API request was handled, therefore the route name found
// // in the wrapped `ensureApiPage` is the route name.
// const trans = ins.currTransaction()
// if (trans && trans[kPossibleApiRouteName]) {
// console.log('XXX could set trans name here')
// trans[kRouteName] = trans[kPossibleApiRouteName]
// }
// }
// }
// )
// return promise
// }
// }
function wrapEnsureApiPage (orig) {
return function wrappedEnsureApiPage (pathname) {
const trans = ins.currTransaction()
if (trans) {
log.trace({ pathname }, 'found possible API route name from ensureApiPage')
trans[kPossibleApiRouteName] = pathname
if (trans && trans.req) {
// XXX slight limitation on "handled" could *possibly* be false if
// exception in `getPagePath`. Trade-off between getting that
// wrong and setting the trans.name early enough for captureError
// usage.
log.trace({ pathname }, 'set transaction name from ensureApiPage')
trans.setDefaultName(`${trans.req.method} ${pathname}`)
}
return orig.apply(this, arguments)
}
@@ -123,22 +136,32 @@ module.exports = function (mod, agent, { version, enabled }) {
promise.then(findComponentsResult => {
if (findComponentsResult) {
const trans = ins.currTransaction()
// Avoid overriding and already set kRouteName for the case when
// there is a page error and `findPageComponents()` is called
// independently to load the error page (e.g. "_error.js" or "500.js").
if (trans && !trans[kRouteName]) {
log.trace({ pathname }, 'found route from findPageComponents')
trans[kRouteName] = pathname
// If Next.js is doing error handling for this request, then it is
// loading an *error* page component (e.g. "_error.js"). We don't want
// to use that component's path for the transaction name.
if (trans && !trans[kInErrorHandling] && trans.req) {
log.trace({ pathname }, 'set transaction name from findPageComponents')
trans.setDefaultName(`${trans.req.method} ${pathname}`)
}
}
})
return promise
}
}

function wrapRenderErrorToResponse (orig) {
return function wrappedRenderErrorToResponse (ctx, err) {
console.log('XXX wrappedRenderErrorToResponse(ctx, err=%s)', err)
agent.captureError(err)
const trans = ins.currTransaction()
if (trans) {
// Signal to subsequent instrumentation for this transaction that
// Next.js is now doing error handling for this request.
trans[kInErrorHandling] = true
}
// Next.js uses `err=null` to handle a 404.
if (err) {
agent.captureError(err)
}
return orig.apply(this, arguments)
}
}
Original file line number Diff line number Diff line change
@@ -2,6 +2,6 @@

// Always executed server-side.
export default function anApiEndpointThatThrows(req, res) {
throw new Error('boom')
throw new Error('An error thrown in anApiEndpointThatThrows handler')
}