From 23345d5047c4e007bcc3b5c79cdd437e2f33fd51 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 9 May 2023 16:08:45 +0000 Subject: [PATCH 1/9] feat(nextjs): Add automatic monitors for Vercel Cron Jobs --- .../src/config/loaders/wrappingLoader.ts | 5 ++++ .../config/templates/apiWrapperTemplate.ts | 10 +++++++- packages/nextjs/src/config/types.ts | 7 ++++++ packages/nextjs/src/config/webpack.ts | 25 +++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/loaders/wrappingLoader.ts b/packages/nextjs/src/config/loaders/wrappingLoader.ts index 2cd83f672fd5..28c8f58b2eb9 100644 --- a/packages/nextjs/src/config/loaders/wrappingLoader.ts +++ b/packages/nextjs/src/config/loaders/wrappingLoader.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { rollup } from 'rollup'; +import type { VercelCronsConfig } from '../../common/types'; import type { LoaderThis } from './types'; // Just a simple placeholder to make referencing module consistent @@ -44,6 +45,7 @@ type LoaderOptions = { excludeServerRoutes: Array; wrappingTargetKind: 'page' | 'api-route' | 'middleware' | 'server-component'; sentryConfigFilePath?: string; + vercelCronsConfig?: VercelCronsConfig; }; function moduleExists(id: string): boolean { @@ -74,6 +76,7 @@ export default function wrappingLoader( excludeServerRoutes = [], wrappingTargetKind, sentryConfigFilePath, + vercelCronsConfig, } = 'getOptions' in this ? this.getOptions() : this.query; this.async(); @@ -113,6 +116,8 @@ export default function wrappingLoader( throw new Error(`Invariant: Could not get template code of unknown kind "${wrappingTargetKind}"`); } + templateCode = templateCode.replace(/__VERCEL_CRONS_CONFIGURATION__/g, JSON.stringify(vercelCronsConfig)); + // Inject the route and the path to the file we're wrapping into the template templateCode = templateCode.replace(/__ROUTE__/g, parameterizedPagesRoute.replace(/\\/g, '\\\\')); } else if (wrappingTargetKind === 'server-component') { diff --git a/packages/nextjs/src/config/templates/apiWrapperTemplate.ts b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts index 91cf5ef1e0c6..26b03ecbb666 100644 --- a/packages/nextjs/src/config/templates/apiWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts @@ -13,6 +13,7 @@ import * as origModule from '__SENTRY_WRAPPING_TARGET_FILE__'; import * as Sentry from '@sentry/nextjs'; import type { PageConfig } from 'next'; +import type { VercelCronsConfig } from '../../common/types'; // We import this from `wrappers` rather than directly from `next` because our version can work simultaneously with // multiple versions of next. See note in `wrappers/types` for more. import type { NextApiHandler } from '../../server/types'; @@ -54,7 +55,14 @@ export const config = { }, }; -export default userProvidedHandler ? Sentry.wrapApiHandlerWithSentry(userProvidedHandler, '__ROUTE__') : undefined; +declare const __VERCEL_CRONS_CONFIGURATION__: VercelCronsConfig; + +export default userProvidedHandler + ? Sentry.wrapApiHandlerWithSentry( + Sentry.wrapApiHandlerWithSentryVercelCrons(userProvidedHandler, __VERCEL_CRONS_CONFIGURATION__), + '__ROUTE__', + ) + : undefined; // 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. diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index f2156382e6f3..68d915ee5180 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -133,6 +133,13 @@ export type UserSentryOptions = { * Tree shakes Sentry SDK logger statements from the bundle. */ disableLogger?: boolean; + + /** + * Automatically create cron monitors in Sentry for your Vercel Cron Jobs if configureed via`vercel.json`. + * + * Defaults to `true`. + */ + automaticMonitors?: boolean; }; export type NextConfigFunction = (phase: string, defaults: { defaultConfig: NextConfigObject }) => NextConfigObject; diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 73fb60660451..ed3ce13101dc 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -7,6 +7,7 @@ import * as chalk from 'chalk'; import * as fs from 'fs'; import * as path from 'path'; +import type { VercelCronsConfig } from '../common/types'; // Note: If you need to import a type from Webpack, do it in `types.ts` and export it from there. Otherwise, our // circular dependency check thinks this file is importing from itself. See https://github.com/pahen/madge/issues/306. import type { @@ -163,6 +164,29 @@ export function constructWebpackConfigFunction( ], }); + let vercelCronsConfig: VercelCronsConfig = undefined; + try { + if (process.env.VERCEL && userSentryOptions.automaticMonitors !== false) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + vercelCronsConfig = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'vercel.json'), 'utf8')).crons; + logger.info( + `${chalk.cyan( + 'info', + )} - Creating Sentry cron monitors for your Vercel Cron Jobs. You can disable this feature by setting the ${chalk.bold.cyan( + 'automaticMonitors', + )} option to false in you Next.js config.`, + ); + } + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (e.code === 'ENOENT') { + // noop if file does not exist + } else { + // log but noop + logger.error(`${chalk.red('error')} - Sentry failed to read vercel.json`, e); + } + } + // Wrap api routes newConfig.module.rules.unshift({ test: resourcePath => { @@ -177,6 +201,7 @@ export function constructWebpackConfigFunction( loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), options: { ...staticWrappingLoaderOptions, + vercelCronsConfig, wrappingTargetKind: 'api-route', }, }, From 64a300321ab240f5af0369ebe9a2a838422883e4 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 9 May 2023 20:08:40 +0000 Subject: [PATCH 2/9] fix edge --- .../wrapApiHandlerWithSentryVercelCrons.ts | 16 +++++++++++----- packages/nextjs/src/edge/edgeclient.ts | 2 ++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts index abf707dd5f2c..a9322067792d 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts @@ -1,8 +1,13 @@ -import { captureCheckIn, runWithAsyncContext } from '@sentry/core'; +import { captureCheckIn, getCurrentHub, runWithAsyncContext } from '@sentry/core'; import type { NextApiRequest } from 'next'; import type { VercelCronsConfig } from './types'; +type EdgeRequest = { + nextUrl: URL; + headers: Headers; +}; + /** * Wraps a function with Sentry crons instrumentation by automaticaly sending check-ins for the given Vercel crons config. */ @@ -11,19 +16,21 @@ export function wrapApiHandlerWithSentryVercelCrons { + apply: (originalFunction, thisArg, args: [NextApiRequest | EdgeRequest | undefined] | undefined) => { return runWithAsyncContext(() => { if (!args || !args[0]) { return originalFunction.apply(thisArg, args); } + const [req] = args; let maybePromiseResult; - const cronsKey = req.url; + const cronsKey = 'nextUrl' in req ? req.nextUrl.pathname : req.url; + const userAgentHeader = 'nextUrl' in req ? req.headers.get('user-agent') : req.headers['user-agent']; if ( !vercelCronsConfig || // do nothing if vercel crons config is missing - !req.headers['user-agent']?.includes('vercel-cron') // do nothing if endpoint is not called from vercel crons + !userAgentHeader?.includes('vercel-cron') // do nothing if endpoint is not called from vercel crons ) { return originalFunction.apply(thisArg, args); } @@ -42,7 +49,6 @@ export function wrapApiHandlerWithSentryVercelCrons { } const envelope = createCheckInEnvelope(serializedCheckIn, this.getSdkMetadata(), tunnel, this.getDsn()); + + __DEBUG_BUILD__ && logger.info('Sending checkin:', checkIn.monitorSlug, checkIn.status); void this._sendEnvelope(envelope); return id; } From bcd2bc23c2c1bea57307e08294a064a7fbcb7be1 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 9 May 2023 20:11:57 +0000 Subject: [PATCH 3/9] . --- .../src/config/templates/apiWrapperTemplate.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/nextjs/src/config/templates/apiWrapperTemplate.ts b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts index 26b03ecbb666..89d870d5e767 100644 --- a/packages/nextjs/src/config/templates/apiWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts @@ -57,12 +57,13 @@ export const config = { declare const __VERCEL_CRONS_CONFIGURATION__: VercelCronsConfig; -export default userProvidedHandler - ? Sentry.wrapApiHandlerWithSentry( - Sentry.wrapApiHandlerWithSentryVercelCrons(userProvidedHandler, __VERCEL_CRONS_CONFIGURATION__), - '__ROUTE__', - ) - : undefined; +let wrappedHandler = userProvidedHandler; + +if (wrappedHandler && __VERCEL_CRONS_CONFIGURATION__) { + wrappedHandler = Sentry.wrapApiHandlerWithSentryVercelCrons(wrappedHandler, __VERCEL_CRONS_CONFIGURATION__); +} + +export default wrappedHandler; // 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. From 5ff80e4ae4e7a33f0b87c07bccaecf3897129ba8 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 9 May 2023 20:31:41 +0000 Subject: [PATCH 4/9] . --- packages/nextjs/src/config/types.ts | 2 +- packages/nextjs/src/config/webpack.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 68d915ee5180..be3a8f250b0b 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -139,7 +139,7 @@ export type UserSentryOptions = { * * Defaults to `true`. */ - automaticMonitors?: boolean; + automaticVercelMonitors?: boolean; }; export type NextConfigFunction = (phase: string, defaults: { defaultConfig: NextConfigObject }) => NextConfigObject; diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index ed3ce13101dc..40a1267d7c34 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -166,14 +166,14 @@ export function constructWebpackConfigFunction( let vercelCronsConfig: VercelCronsConfig = undefined; try { - if (process.env.VERCEL && userSentryOptions.automaticMonitors !== false) { + if (process.env.VERCEL && userSentryOptions.automaticVercelMonitors !== false) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access vercelCronsConfig = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'vercel.json'), 'utf8')).crons; logger.info( `${chalk.cyan( 'info', )} - Creating Sentry cron monitors for your Vercel Cron Jobs. You can disable this feature by setting the ${chalk.bold.cyan( - 'automaticMonitors', + 'automaticVercelMonitors', )} option to false in you Next.js config.`, ); } From aaa8206ad0ecc56b1448a133b9aa098b57c75642 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 9 May 2023 20:43:24 +0000 Subject: [PATCH 5/9] . --- .../nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts index a9322067792d..3b9bc8ca7045 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts @@ -1,4 +1,4 @@ -import { captureCheckIn, getCurrentHub, runWithAsyncContext } from '@sentry/core'; +import { captureCheckIn, runWithAsyncContext } from '@sentry/core'; import type { NextApiRequest } from 'next'; import type { VercelCronsConfig } from './types'; From 4b8f309aa7d108850eec6954ff3db212955b2a43 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 9 May 2023 20:45:55 +0000 Subject: [PATCH 6/9] . --- packages/nextjs/src/config/templates/apiWrapperTemplate.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/nextjs/src/config/templates/apiWrapperTemplate.ts b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts index 89d870d5e767..0eccf3024a76 100644 --- a/packages/nextjs/src/config/templates/apiWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts @@ -59,6 +59,10 @@ declare const __VERCEL_CRONS_CONFIGURATION__: VercelCronsConfig; let wrappedHandler = userProvidedHandler; +if (wrappedHandler) { + wrappedHandler = Sentry.wrapApiHandlerWithSentry(wrappedHandler, '__ROUTE__'); +} + if (wrappedHandler && __VERCEL_CRONS_CONFIGURATION__) { wrappedHandler = Sentry.wrapApiHandlerWithSentryVercelCrons(wrappedHandler, __VERCEL_CRONS_CONFIGURATION__); } From 1e1711d64b712c1b282d6cfb6ec285da895719b7 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 10 May 2023 14:35:26 +0200 Subject: [PATCH 7/9] Update packages/nextjs/src/config/types.ts Co-authored-by: Abhijeet Prasad --- packages/nextjs/src/config/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index be3a8f250b0b..b12fbbed94be 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -135,7 +135,7 @@ export type UserSentryOptions = { disableLogger?: boolean; /** - * Automatically create cron monitors in Sentry for your Vercel Cron Jobs if configureed via`vercel.json`. + * Automatically create cron monitors in Sentry for your Vercel Cron Jobs if configured via`vercel.json`. * * Defaults to `true`. */ From 9d187863c7ae2ea2bfca2e1c693f1749d01385df Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 10 May 2023 14:41:14 +0200 Subject: [PATCH 8/9] Update packages/nextjs/src/config/types.ts Co-authored-by: Abhijeet Prasad --- packages/nextjs/src/config/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index b12fbbed94be..28f70d62dc05 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -135,7 +135,7 @@ export type UserSentryOptions = { disableLogger?: boolean; /** - * Automatically create cron monitors in Sentry for your Vercel Cron Jobs if configured via`vercel.json`. + * Automatically create cron monitors in Sentry for your Vercel Cron Jobs if configured via `vercel.json`. * * Defaults to `true`. */ From 49f334f83d859670d2e968317cb202f7d16edf92 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 10 May 2023 12:47:27 +0000 Subject: [PATCH 9/9] Only show message when crons field is defined --- packages/nextjs/src/config/webpack.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 40a1267d7c34..0bb42f98b7ec 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -169,13 +169,15 @@ export function constructWebpackConfigFunction( if (process.env.VERCEL && userSentryOptions.automaticVercelMonitors !== false) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access vercelCronsConfig = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'vercel.json'), 'utf8')).crons; - logger.info( - `${chalk.cyan( - 'info', - )} - Creating Sentry cron monitors for your Vercel Cron Jobs. You can disable this feature by setting the ${chalk.bold.cyan( - 'automaticVercelMonitors', - )} option to false in you Next.js config.`, - ); + if (vercelCronsConfig) { + logger.info( + `${chalk.cyan( + 'info', + )} - Creating Sentry cron monitors for your Vercel Cron Jobs. You can disable this feature by setting the ${chalk.bold.cyan( + 'automaticVercelMonitors', + )} option to false in you Next.js config.`, + ); + } } } catch (e) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access