diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts index abf707dd5f2c..3b9bc8ca7045 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts @@ -3,6 +3,11 @@ 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; 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..0eccf3024a76 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,19 @@ export const config = { }, }; -export default userProvidedHandler ? Sentry.wrapApiHandlerWithSentry(userProvidedHandler, '__ROUTE__') : undefined; +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__); +} + +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. diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index f2156382e6f3..28f70d62dc05 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 configured via `vercel.json`. + * + * Defaults to `true`. + */ + 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 73fb60660451..0bb42f98b7ec 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,31 @@ export function constructWebpackConfigFunction( ], }); + let vercelCronsConfig: VercelCronsConfig = undefined; + try { + 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; + 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 + 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 +203,7 @@ export function constructWebpackConfigFunction( loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), options: { ...staticWrappingLoaderOptions, + vercelCronsConfig, wrappingTargetKind: 'api-route', }, }, diff --git a/packages/nextjs/src/edge/edgeclient.ts b/packages/nextjs/src/edge/edgeclient.ts index 64e3d526061c..ce13d6666448 100644 --- a/packages/nextjs/src/edge/edgeclient.ts +++ b/packages/nextjs/src/edge/edgeclient.ts @@ -104,6 +104,8 @@ export class EdgeClient extends BaseClient { } 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; }