-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(nextjs): Auto-wrap API routes (#5778)
As part of #5505, this applies to API route handlers the same kind of auto-wrapping we've done with the data fetchers (`withServerSideProps` and the like). Though the general idea is the same, the one extra complicating factor here is that there's a good chance the handlers get to us already wrapped in `withSentry`, which we've up until now been telling users to use as a manual wrapper. This is handled by making `withSentry` idempotent - if it detects that it's already been run on the current request, it simply acts as a pass-through to the function it wraps. Notes: - A new template has been created to act as a proxy module for API routes, but the proxying work itself is done by the same `proxyLoader` as before - it just loads one template or the other depending on an individual page's path. - Doing this auto-wrapping gives us a chance to do one thing manual `withSentry` wrapping isn't able to do, which is set the [route config](https://nextjs.org/docs/api-routes/request-helpers) to use an external resolver, which will prevent next's dev server from throwing warnings about API routes not sending responses. (In other words, it should solve #3852.)
- Loading branch information
1 parent
036e2a0
commit 1d8370e
Showing
16 changed files
with
260 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
packages/nextjs/src/config/templates/apiProxyLoaderTemplate.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
/** | ||
* This file is a template for the code which will be substituted when our webpack loader handles API files in the | ||
* `pages/` directory. | ||
* | ||
* We use `__RESOURCE_PATH__` as a placeholder for the path to the file being wrapped. Because it's not a real package, | ||
* this causes both TS and ESLint to complain, hence the pragma comments below. | ||
*/ | ||
|
||
// @ts-ignore See above | ||
// eslint-disable-next-line import/no-unresolved | ||
import * as origModule from '__RESOURCE_PATH__'; | ||
import * as Sentry from '@sentry/nextjs'; | ||
import type { PageConfig } from 'next'; | ||
|
||
// We import this from `withSentry` rather than directly from `next` because our version can work simultaneously with | ||
// multiple versions of next. See note in `withSentry` for more. | ||
import type { NextApiHandler } from '../../utils/withSentry'; | ||
|
||
type NextApiModule = { | ||
default: NextApiHandler; | ||
config?: PageConfig; | ||
}; | ||
|
||
const userApiModule = origModule as NextApiModule; | ||
|
||
const maybeWrappedHandler = userApiModule.default; | ||
const origConfig = userApiModule.config || {}; | ||
|
||
// Setting `externalResolver` to `true` prevents nextjs from throwing a warning in dev about API routes resolving | ||
// without sending a response. It's a false positive (a response is sent, but only after we flush our send queue), and | ||
// we throw a warning of our own to tell folks that, but it's better if we just don't have to deal with it in the first | ||
// place. | ||
export const config = { | ||
...origConfig, | ||
api: { | ||
...origConfig.api, | ||
externalResolver: true, | ||
}, | ||
}; | ||
|
||
export default Sentry.withSentryAPI(maybeWrappedHandler, '__ROUTE__'); | ||
|
||
// Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to | ||
// not include anything whose name matchs something we've explicitly exported above. | ||
// @ts-ignore See above | ||
// eslint-disable-next-line import/no-unresolved | ||
export * from '__RESOURCE_PATH__'; |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { formatAsCode, nextLogger } from '../../utils/nextLogger'; | ||
// We import these types from `withSentry` rather than directly from `next` because our version can work simultaneously | ||
// with multiple versions of next. See note in `withSentry` for more. | ||
import type { NextApiHandler, WrappedNextApiHandler } from '../../utils/withSentry'; | ||
import { withSentry } from '../../utils/withSentry'; | ||
|
||
/** | ||
* Wrap the given API route handler for tracing and error capturing. Thin wrapper around `withSentry`, which only | ||
* applies it if it hasn't already been applied. | ||
* | ||
* @param maybeWrappedHandler The handler exported from the user's API page route file, which may or may not already be | ||
* wrapped with `withSentry` | ||
* @param parameterizedRoute The page's route, passed in via the proxy loader | ||
* @returns The wrapped handler | ||
*/ | ||
export function withSentryAPI( | ||
maybeWrappedHandler: NextApiHandler | WrappedNextApiHandler, | ||
parameterizedRoute: string, | ||
): WrappedNextApiHandler { | ||
// Log a warning if the user is still manually wrapping their route in `withSentry`. Doesn't work in cases where | ||
// there's been an intermediate wrapper (like `withSentryAPI(someOtherWrapper(withSentry(handler)))`) but should catch | ||
// most cases. Only runs once per route. (Note: Such double-wrapping isn't harmful, but we'll eventually deprecate and remove `withSentry`, so | ||
// best to get people to stop using it.) | ||
if (maybeWrappedHandler.name === 'sentryWrappedHandler') { | ||
const [_sentryNextjs_, _autoWrapOption_, _withSentry_, _route_] = [ | ||
'@sentry/nextjs', | ||
'autoInstrumentServerFunctions', | ||
'withSentry', | ||
parameterizedRoute, | ||
].map(phrase => formatAsCode(phrase)); | ||
|
||
nextLogger.info( | ||
`${_sentryNextjs_} is running with the ${_autoWrapOption_} flag set, which means API routes no longer need to ` + | ||
`be manually wrapped with ${_withSentry_}. Detected manual wrapping in ${_route_}.`, | ||
); | ||
} | ||
|
||
return withSentry(maybeWrappedHandler, parameterizedRoute); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/* eslint-disable no-console */ | ||
import * as chalk from 'chalk'; | ||
|
||
// This is nextjs's own logging formatting, vendored since it's not exported. See | ||
// https://github.com/vercel/next.js/blob/c3ceeb03abb1b262032bd96457e224497d3bbcef/packages/next/build/output/log.ts#L3-L11 | ||
// and | ||
// https://github.com/vercel/next.js/blob/de7aa2d6e486c40b8be95a1327639cbed75a8782/packages/next/lib/eslint/runLintCheck.ts#L321-L323. | ||
|
||
const prefixes = { | ||
wait: `${chalk.cyan('wait')} -`, | ||
error: `${chalk.red('error')} -`, | ||
warn: `${chalk.yellow('warn')} -`, | ||
ready: `${chalk.green('ready')} -`, | ||
info: `${chalk.cyan('info')} -`, | ||
event: `${chalk.magenta('event')} -`, | ||
trace: `${chalk.magenta('trace')} -`, | ||
}; | ||
|
||
export const formatAsCode = (str: string): string => chalk.bold.cyan(str); | ||
|
||
export const nextLogger: { | ||
[key: string]: (...message: unknown[]) => void; | ||
} = { | ||
wait: (...message) => console.log(prefixes.wait, ...message), | ||
error: (...message) => console.error(prefixes.error, ...message), | ||
warn: (...message) => console.warn(prefixes.warn, ...message), | ||
ready: (...message) => console.log(prefixes.ready, ...message), | ||
info: (...message) => console.log(prefixes.info, ...message), | ||
event: (...message) => console.log(prefixes.event, ...message), | ||
trace: (...message) => console.log(prefixes.trace, ...message), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
packages/nextjs/test/integration/pages/api/withSentryAPI/unwrapped/[...pathParts].ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { NextApiRequest, NextApiResponse } from 'next'; | ||
|
||
const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise<void> => { | ||
res.status(200).json({}); | ||
}; | ||
|
||
export default handler; |
7 changes: 7 additions & 0 deletions
7
packages/nextjs/test/integration/pages/api/withSentryAPI/unwrapped/[animal].ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { NextApiRequest, NextApiResponse } from 'next'; | ||
|
||
const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise<void> => { | ||
res.status(200).json({}); | ||
}; | ||
|
||
export default handler; |
7 changes: 7 additions & 0 deletions
7
packages/nextjs/test/integration/pages/api/withSentryAPI/unwrapped/noParams.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { NextApiRequest, NextApiResponse } from 'next'; | ||
|
||
const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise<void> => { | ||
res.status(200).json({}); | ||
}; | ||
|
||
export default handler; |
8 changes: 8 additions & 0 deletions
8
packages/nextjs/test/integration/pages/api/withSentryAPI/wrapped/[...pathParts].ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { withSentry } from '@sentry/nextjs'; | ||
import { NextApiRequest, NextApiResponse } from 'next'; | ||
|
||
const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise<void> => { | ||
res.status(200).json({}); | ||
}; | ||
|
||
export default withSentry(handler); |
8 changes: 8 additions & 0 deletions
8
packages/nextjs/test/integration/pages/api/withSentryAPI/wrapped/[animal].ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { withSentry } from '@sentry/nextjs'; | ||
import { NextApiRequest, NextApiResponse } from 'next'; | ||
|
||
const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise<void> => { | ||
res.status(200).json({}); | ||
}; | ||
|
||
export default withSentry(handler); |
8 changes: 8 additions & 0 deletions
8
packages/nextjs/test/integration/pages/api/withSentryAPI/wrapped/noParams.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { withSentry } from '@sentry/nextjs'; | ||
import { NextApiRequest, NextApiResponse } from 'next'; | ||
|
||
const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise<void> => { | ||
res.status(200).json({}); | ||
}; | ||
|
||
export default withSentry(handler); |
54 changes: 54 additions & 0 deletions
54
packages/nextjs/test/integration/test/server/tracingWithSentryAPI.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
const assert = require('assert'); | ||
|
||
const { sleep } = require('../utils/common'); | ||
const { getAsync, interceptTracingRequest } = require('../utils/server'); | ||
|
||
module.exports = async ({ url: urlBase, argv }) => { | ||
const urls = { | ||
// testName: [url, route] | ||
unwrappedNoParamURL: [`/api/withSentryAPI/unwrapped/noParams`, '/api/withSentryAPI/unwrapped/noParams'], | ||
unwrappedDynamicURL: [`/api/withSentryAPI/unwrapped/dog`, '/api/withSentryAPI/unwrapped/[animal]'], | ||
unwrappedCatchAllURL: [`/api/withSentryAPI/unwrapped/dog/facts`, '/api/withSentryAPI/unwrapped/[...pathParts]'], | ||
wrappedNoParamURL: [`/api/withSentryAPI/wrapped/noParams`, '/api/withSentryAPI/wrapped/noParams'], | ||
wrappedDynamicURL: [`/api/withSentryAPI/wrapped/dog`, '/api/withSentryAPI/wrapped/[animal]'], | ||
wrappedCatchAllURL: [`/api/withSentryAPI/wrapped/dog/facts`, '/api/withSentryAPI/wrapped/[...pathParts]'], | ||
}; | ||
|
||
const interceptedRequests = {}; | ||
|
||
Object.entries(urls).forEach(([testName, [url, route]]) => { | ||
interceptedRequests[testName] = interceptTracingRequest( | ||
{ | ||
contexts: { | ||
trace: { | ||
op: 'http.server', | ||
status: 'ok', | ||
tags: { 'http.status_code': '200' }, | ||
}, | ||
}, | ||
transaction: `GET ${route}`, | ||
type: 'transaction', | ||
request: { | ||
url: `${urlBase}${url}`, | ||
}, | ||
}, | ||
argv, | ||
testName, | ||
); | ||
}); | ||
|
||
// Wait until all requests have completed | ||
await Promise.all(Object.values(urls).map(([url]) => getAsync(`${urlBase}${url}`))); | ||
|
||
await sleep(250); | ||
|
||
const failingTests = Object.entries(interceptedRequests).reduce( | ||
(failures, [testName, request]) => (!request.isDone() ? failures.concat(testName) : failures), | ||
[], | ||
); | ||
|
||
assert.ok( | ||
failingTests.length === 0, | ||
`Did not intercept transaction request for the following tests: ${failingTests.join(', ')}.`, | ||
); | ||
}; |