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(nextjs): Add automatic monitors for Vercel Cron Jobs #8088

Merged
merged 9 commits into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 10 additions & 4 deletions packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -11,19 +16,21 @@ export function wrapApiHandlerWithSentryVercelCrons<F extends (...args: any[]) =
vercelCronsConfig: VercelCronsConfig,
): F {
return new Proxy(handler, {
apply: (originalFunction, thisArg, args: [NextApiRequest | undefined] | undefined) => {
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);
}
Expand All @@ -42,7 +49,6 @@ export function wrapApiHandlerWithSentryVercelCrons<F extends (...args: any[]) =
status: 'in_progress',
},
{
checkinMargin: 2, // two minutes - in case Vercel has a blip
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we remove this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a discussion with the crons team. The missed checkins were a bug in the product when the margin is set to 0.

maxRuntime: 60 * 12, // (minutes) so 12 hours - just a very high arbitrary number since we don't know the actual duration of the users cron job
schedule: {
type: 'crontab',
Expand Down
5 changes: 5 additions & 0 deletions packages/nextjs/src/config/loaders/wrappingLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -44,6 +45,7 @@ type LoaderOptions = {
excludeServerRoutes: Array<RegExp | string>;
wrappingTargetKind: 'page' | 'api-route' | 'middleware' | 'server-component';
sentryConfigFilePath?: string;
vercelCronsConfig?: VercelCronsConfig;
};

function moduleExists(id: string): boolean {
Expand Down Expand Up @@ -74,6 +76,7 @@ export default function wrappingLoader(
excludeServerRoutes = [],
wrappingTargetKind,
sentryConfigFilePath,
vercelCronsConfig,
} = 'getOptions' in this ? this.getOptions() : this.query;

this.async();
Expand Down Expand Up @@ -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') {
Expand Down
15 changes: 14 additions & 1 deletion packages/nextjs/src/config/templates/apiWrapperTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
lforst marked this conversation as resolved.
Show resolved Hide resolved
*
* Defaults to `true`.
*/
automaticVercelMonitors?: boolean;
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
};

export type NextConfigFunction = (phase: string, defaults: { defaultConfig: NextConfigObject }) => NextConfigObject;
Expand Down
25 changes: 25 additions & 0 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -163,6 +164,29 @@ 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;
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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: Should we only log this out if the crons field is defined? Otherwise I can see how this might get confusing

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'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 => {
Expand All @@ -177,6 +201,7 @@ export function constructWebpackConfigFunction(
loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
options: {
...staticWrappingLoaderOptions,
vercelCronsConfig,
wrappingTargetKind: 'api-route',
},
},
Expand Down
2 changes: 2 additions & 0 deletions packages/nextjs/src/edge/edgeclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export class EdgeClient extends BaseClient<EdgeClientOptions> {
}

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;
}
Expand Down