UNPKG

elastic-apm-node

Version:

The official Elastic APM agent for Node.js

197 lines (178 loc) 6.63 kB
/* * Copyright Elasticsearch B.V. and other contributors where applicable. * Licensed under the BSD 2-Clause License; you may not use this file except in * compliance with the BSD 2-Clause License. */ 'use strict'; // See "lib/instrumentation/modules/next/README.md". const semver = require('semver'); const shimmer = require('../../../../../shimmer'); // `kSetTransNameFn` symbol is shared with "next-server.js" instrumentation. const kSetTransNameFn = Symbol.for('ElasticAPMNextJsSetTransNameFn'); const noopFn = () => {}; module.exports = function (mod, agent, { version, enabled }) { if (!enabled) { return mod; } if ( !semver.satisfies(version, '>=12.0.0 <13.3.0', { includePrerelease: true }) ) { agent.logger.debug('next version %s not supported, skipping', version); return mod; } const ins = agent._instrumentation; const log = agent.logger; const DevServer = mod.default; shimmer.wrap(DevServer.prototype, 'generateRoutes', wrapGenerateRoutes); shimmer.wrap( DevServer.prototype, 'findPageComponents', wrapFindPageComponents, ); // Instrumenting the DevServer also uses the wrapping of // 'NextNodeServer.renderErrorToResponse' in "next-server.js". return mod; // The `route` objects being wrapped here have this type: // https://github.com/vercel/next.js/blob/v12.3.0/packages/next/server/router.ts#L26-L45 function wrapGenerateRoutes(orig) { return function wrappedGenerateRoutes() { if (this.constructor !== DevServer) { return orig.apply(this, arguments); } const routes = orig.apply(this, arguments); log.debug('wrap Next.js DevServer routes'); routes.redirects.forEach(wrapRedirectRoute); routes.rewrites.beforeFiles.forEach(wrapRewriteRoute); routes.rewrites.afterFiles.forEach(wrapRewriteRoute); routes.rewrites.fallback.forEach(wrapRewriteRoute); routes.fsRoutes.forEach(wrapFsRoute); wrapCatchAllRoute(routes.catchAllRoute); return routes; }; } function wrapRedirectRoute(route) { if (typeof route.fn !== 'function') { return; } const origRouteFn = route.fn; route.fn = function () { const trans = ins.currTransaction(); if (trans) { trans.setDefaultName('Next.js ' + route.name); trans[kSetTransNameFn] = noopFn; } return origRouteFn.apply(this, arguments); }; } function wrapRewriteRoute(route) { if (typeof route.fn !== 'function') { return; } const origRouteFn = route.fn; route.fn = function () { const trans = ins.currTransaction(); if (trans) { trans.setDefaultName(`Next.js ${route.name} -> ${route.destination}`); trans[kSetTransNameFn] = noopFn; } return origRouteFn.apply(this, arguments); }; } // "FS" routes are those that go looking for matching paths on the filesystem // to fulfill the request. function wrapFsRoute(route) { if (typeof route.fn !== 'function') { return; } const origRouteFn = route.fn; // We explicitly handle only the `fsRoute`s that we know by name in the // Next.js code. We cannot set `trans.name` for all of them because of the // true catch-all-routes that match any path and only sometimes handle them // (e.g. 'public folder catchall'). switch (route.name) { case '_next/data catchall': // This handles "/_next/data/..." paths that are used by Next.js // client-side code to call `getServerSideProps()` for user pages. route.fn = function () { const trans = ins.currTransaction(); if (trans) { trans.setDefaultName(`Next.js ${route.name}`); if (!trans[kSetTransNameFn]) { trans[kSetTransNameFn] = (_req, pathname) => { trans.setDefaultName(`Next.js _next/data route ${pathname}`); trans[kSetTransNameFn] = noopFn; }; } } return origRouteFn.apply(this, arguments); }; break; case '_next/static/development/_devMiddlewareManifest.json': case '_next/static/development/_devPagesManifest.json': case '_next/development catchall': case '_next/static catchall': case '_next/image catchall': case '_next catchall': route.fn = function () { const trans = ins.currTransaction(); if (trans) { trans.setDefaultName(`Next.js ${route.name}`); } return origRouteFn.apply(this, arguments); }; break; } } function wrapCatchAllRoute(route) { if (typeof route.fn !== 'function') { return; } const origRouteFn = route.fn; route.fn = function () { const trans = ins.currTransaction(); // This is a catchall route, so only set a kSetTransNameFn if a more // specific route wrapper hasn't already done so. if (trans && !trans[kSetTransNameFn]) { trans[kSetTransNameFn] = (req, pathname) => { trans.setDefaultName(`${req.method} ${pathname}`); // Ensure only the first `findPageComponents` result sets the trans // name, otherwise a loaded `/_error` for page error handling could // incorrectly override. trans[kSetTransNameFn] = noopFn; }; } return origRouteFn.apply(this, arguments); }; } // `findPageComponents` is used to load any "./pages/..." files. It provides // the resolved path appropriate for the transaction name. function wrapFindPageComponents(orig) { return function wrappedFindPageComponents(pathnameOrArgs) { if (this.constructor !== DevServer) { return orig.apply(this, arguments); } // In next <=12.2.6-canary.10 the function signature is: // async findPageComponents(pathname, query, params, isAppPath) // after that version it is: // async findPageComponents({ pathname, query, params, isAppPath }) const pathname = typeof pathnameOrArgs === 'string' ? pathnameOrArgs : pathnameOrArgs.pathname; const promise = orig.apply(this, arguments); promise.then((findComponentsResult) => { if (findComponentsResult) { const trans = ins.currTransaction(); if (trans && trans.req && trans[kSetTransNameFn]) { log.trace( { pathname }, 'set transaction name from findPageComponents', ); trans[kSetTransNameFn](trans.req, pathname); } } }); return promise; }; } };