Skip to content

Commit f81c82d

Browse files
committed
fix: bring back the previous behavior for compatability
1 parent bca3c6f commit f81c82d

File tree

5 files changed

+101
-49
lines changed

5 files changed

+101
-49
lines changed

packages/nextjs/src/config/webpack.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {
2222
WebpackEntryProperty,
2323
} from './types';
2424
import { getNextjsVersion } from './util';
25+
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';
2526

2627
// Next.js runs webpack 3 times, once for the client, the server, and for edge. Because we don't want to print certain
2728
// warnings 3 times, we keep track of them here.
@@ -46,15 +47,15 @@ export function constructWebpackConfigFunction({
4647
routeManifest,
4748
nextJsVersion,
4849
useRunAfterProductionCompileHook,
49-
vercelCronsConfig,
50+
vercelCronsConfigResult,
5051
}: {
5152
userNextConfig: NextConfigObject;
5253
userSentryOptions: SentryBuildOptions;
5354
releaseName: string | undefined;
5455
routeManifest: RouteManifest | undefined;
5556
nextJsVersion: string | undefined;
5657
useRunAfterProductionCompileHook: boolean | undefined;
57-
vercelCronsConfig: VercelCronsConfig;
58+
vercelCronsConfigResult: VercelCronsConfigResult;
5859
}): WebpackConfigFunction {
5960
// Will be called by nextjs and passed its default webpack configuration and context data about the build (whether
6061
// we're building server or client, whether we're in dev, what version of webpack we're using, etc). Note that
@@ -99,6 +100,13 @@ export function constructWebpackConfigFunction({
99100
// `newConfig.module.rules` is required, so we don't have to keep asserting its existence
100101
const newConfig = setUpModuleRules(rawNewConfig);
101102

103+
// Determine which cron config to use based on strategy
104+
// - 'spans': injected as global for span-based detection (App Router + Pages Router)
105+
// - 'wrapper': passed to wrapping loader for Pages Router API handler wrapping
106+
const { strategy: cronsStrategy, config: cronsConfig } = vercelCronsConfigResult;
107+
const vercelCronsConfigForGlobal = cronsStrategy === 'spans' ? cronsConfig : undefined;
108+
const vercelCronsConfigForWrapper = cronsStrategy === 'wrapper' ? cronsConfig : undefined;
109+
102110
// Add a loader which will inject code that sets global values
103111
addValueInjectionLoader({
104112
newConfig,
@@ -108,7 +116,7 @@ export function constructWebpackConfigFunction({
108116
releaseName,
109117
routeManifest,
110118
nextJsVersion,
111-
vercelCronsConfig,
119+
vercelCronsConfig: vercelCronsConfigForGlobal,
112120
});
113121

114122
addOtelWarningIgnoreRule(newConfig);
@@ -249,7 +257,7 @@ export function constructWebpackConfigFunction({
249257
loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
250258
options: {
251259
...staticWrappingLoaderOptions,
252-
vercelCronsConfig,
260+
vercelCronsConfig: vercelCronsConfigForWrapper,
253261
wrappingTargetKind: 'api-route',
254262
},
255263
},

packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function getFinalConfigObject(
4545
}
4646

4747
const routeManifest = maybeCreateRouteManifest(incomingUserNextConfigObject, userSentryOptions);
48-
const vercelCronsConfig = maybeGetVercelCronsConfig(userSentryOptions);
48+
const vercelCronsConfigResult = maybeGetVercelCronsConfig(userSentryOptions);
4949
setUpBuildTimeVariables(incomingUserNextConfigObject, userSentryOptions, releaseName);
5050

5151
const nextJsVersion = getNextjsVersion();
@@ -65,7 +65,7 @@ export function getFinalConfigObject(
6565
routeManifest,
6666
nextJsVersion,
6767
bundlerInfo,
68-
vercelCronsConfig,
68+
vercelCronsConfigResult,
6969
);
7070

7171
const shouldUseRunAfterProductionCompileHook = resolveUseRunAfterProductionCompileHookOption(
@@ -96,7 +96,7 @@ export function getFinalConfigObject(
9696
nextJsVersion,
9797
shouldUseRunAfterProductionCompileHook,
9898
bundlerInfo,
99-
vercelCronsConfig,
99+
vercelCronsConfigResult,
100100
}),
101101
...getTurbopackPatch(bundlerInfo, turboPackConfig),
102102
};

packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { VercelCronsConfig } from '../../common/types';
21
import { handleRunAfterProductionCompile } from '../handleRunAfterProductionCompile';
32
import type { RouteManifest } from '../manifest/types';
43
import { constructTurbopackConfig } from '../turbopack';
54
import type { NextConfigObject, SentryBuildOptions, TurbopackOptions } from '../types';
65
import { detectActiveBundler, supportsProductionCompileHook } from '../util';
76
import { constructWebpackConfigFunction } from '../webpack';
87
import { DEFAULT_SERVER_EXTERNAL_PACKAGES } from './constants';
8+
import type { VercelCronsConfigResult } from './getFinalConfigObjectUtils';
99

1010
/**
1111
* Information about the active bundler and feature support based on Next.js version.
@@ -71,12 +71,15 @@ export function maybeConstructTurbopackConfig(
7171
routeManifest: RouteManifest | undefined,
7272
nextJsVersion: string | undefined,
7373
bundlerInfo: BundlerInfo,
74-
vercelCronsConfig: VercelCronsConfig,
74+
vercelCronsConfigResult: VercelCronsConfigResult,
7575
): TurbopackOptions | undefined {
7676
if (!bundlerInfo.isTurbopack) {
7777
return undefined;
7878
}
7979

80+
// Only pass crons config if the span-based approach is enabled
81+
const vercelCronsConfig = vercelCronsConfigResult.strategy === 'spans' ? vercelCronsConfigResult.config : undefined;
82+
8083
return constructTurbopackConfig({
8184
userNextConfig: incomingUserNextConfigObject,
8285
userSentryOptions,
@@ -254,7 +257,7 @@ export function getWebpackPatch({
254257
nextJsVersion,
255258
shouldUseRunAfterProductionCompileHook,
256259
bundlerInfo,
257-
vercelCronsConfig,
260+
vercelCronsConfigResult,
258261
}: {
259262
incomingUserNextConfigObject: NextConfigObject;
260263
userSentryOptions: SentryBuildOptions;
@@ -263,7 +266,7 @@ export function getWebpackPatch({
263266
nextJsVersion: string | undefined;
264267
shouldUseRunAfterProductionCompileHook: boolean;
265268
bundlerInfo: BundlerInfo;
266-
vercelCronsConfig: VercelCronsConfig;
269+
vercelCronsConfigResult: VercelCronsConfigResult;
267270
}): Partial<NextConfigObject> {
268271
if (!bundlerInfo.isWebpack || userSentryOptions.webpack?.disableSentryConfig) {
269272
return {};
@@ -277,7 +280,7 @@ export function getWebpackPatch({
277280
routeManifest,
278281
nextJsVersion,
279282
useRunAfterProductionCompileHook: shouldUseRunAfterProductionCompileHook,
280-
vercelCronsConfig,
283+
vercelCronsConfigResult,
281284
}),
282285
};
283286
}

packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -252,37 +252,92 @@ export function getNextMajor(nextJsVersion: string | undefined): number | undefi
252252
}
253253

254254
/**
255-
* Reads and returns the Vercel crons configuration from vercel.json.
256-
* Returns undefined if not running on Vercel, if the option is disabled,
257-
* or if vercel.json doesn't exist or doesn't contain crons.
255+
* Reads the Vercel crons configuration from vercel.json.
256+
* Returns undefined if vercel.json doesn't exist or doesn't contain crons.
258257
*/
259-
export function maybeGetVercelCronsConfig(userSentryOptions: SentryBuildOptions): VercelCronsConfig {
260-
// Only read crons config if running on Vercel and the experimental option is enabled
261-
if (!process.env.VERCEL || !userSentryOptions._experimental?.vercelCronsMonitoring) {
262-
return undefined;
263-
}
264-
258+
function readVercelCronsConfig(): VercelCronsConfig {
265259
try {
266260
const vercelJsonPath = path.join(process.cwd(), 'vercel.json');
267261
const vercelJsonContents = fs.readFileSync(vercelJsonPath, 'utf8');
268262
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
269263
const cronsConfig = JSON.parse(vercelJsonContents).crons as VercelCronsConfig;
270264

271265
if (cronsConfig && Array.isArray(cronsConfig) && cronsConfig.length > 0) {
272-
debug.log(
273-
"[@sentry/nextjs] Creating Sentry cron monitors for your Vercel Cron Jobs. You can disable this feature by setting the 'automaticVercelMonitors' option to false in your Next.js config.",
274-
);
275266
return cronsConfig;
276267
}
277-
278268
return undefined;
279269
} catch (e) {
280270
if ((e as { code: string }).code === 'ENOENT') {
281-
// noop if file does not exist
282271
return undefined;
283272
}
284-
// log but noop
285273
debug.error('[@sentry/nextjs] Failed to read vercel.json for automatic cron job monitoring instrumentation', e);
286274
return undefined;
287275
}
288276
}
277+
278+
/** Strategy for Vercel cron monitoring instrumentation */
279+
export type VercelCronsStrategy = 'spans' | 'wrapper';
280+
281+
export type VercelCronsConfigResult = {
282+
/** The crons configuration from vercel.json, if available */
283+
config: VercelCronsConfig;
284+
/**
285+
* The instrumentation strategy to use:
286+
* - `spans`: New span-based approach (works for both App Router and Pages Router)
287+
* - `wrapper`: Old wrapper-based approach (Pages Router only)
288+
* - `undefined`: No cron monitoring enabled
289+
*/
290+
strategy: VercelCronsStrategy | undefined;
291+
};
292+
293+
/**
294+
* Reads and returns the Vercel crons configuration from vercel.json along with
295+
* information about which instrumentation approach to use.
296+
*
297+
* - `_experimental.vercelCronsMonitoring`: New span-based approach (works for both App Router and Pages Router)
298+
* - `automaticVercelMonitors`: Old wrapper-based approach (Pages Router only)
299+
*
300+
* If both are enabled, the new approach is preferred and a warning is logged.
301+
*/
302+
export function maybeGetVercelCronsConfig(userSentryOptions: SentryBuildOptions): VercelCronsConfigResult {
303+
const result: VercelCronsConfigResult = { config: undefined, strategy: undefined };
304+
305+
if (!process.env.VERCEL) {
306+
return result;
307+
}
308+
309+
const experimentalEnabled = userSentryOptions._experimental?.vercelCronsMonitoring === true;
310+
const legacyEnabled = userSentryOptions.webpack?.automaticVercelMonitors === true;
311+
312+
if (!experimentalEnabled && !legacyEnabled) {
313+
return result;
314+
}
315+
316+
const config = readVercelCronsConfig();
317+
if (!config) {
318+
return result;
319+
}
320+
321+
result.config = config;
322+
323+
if (experimentalEnabled && legacyEnabled) {
324+
debug.warn(
325+
"[@sentry/nextjs] Both '_experimental.vercelCronsMonitoring' and 'webpack.automaticVercelMonitors' are enabled. " +
326+
"Using the new span-based approach from '_experimental.vercelCronsMonitoring'. " +
327+
"You can remove 'webpack.automaticVercelMonitors' from your config.",
328+
);
329+
result.strategy = 'spans';
330+
} else if (experimentalEnabled) {
331+
debug.log(
332+
'[@sentry/nextjs] Creating Sentry cron monitors for your Vercel Cron Jobs using span-based instrumentation.',
333+
);
334+
result.strategy = 'spans';
335+
} else {
336+
debug.log(
337+
"[@sentry/nextjs] Creating Sentry cron monitors for your Vercel Cron Jobs. You can disable this feature by setting the 'automaticVercelMonitors' option to false in your Next.js config.",
338+
);
339+
result.strategy = 'wrapper';
340+
}
341+
342+
return result;
343+
}

packages/nextjs/src/server/vercelCronsMonitoring.ts

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ export function maybeStartCronCheckIn(span: Span, route: string | undefined): vo
3636
return;
3737
}
3838

39-
// Get headers from the isolation scope
39+
// The strategy here is to check if the request is a Vercel cron
40+
// request by checking the user agent, vercel always sets the user agent to 'vercel-cron/1.0'
41+
4042
const headers = getIsolationScope().getScopeData().sdkProcessingMetadata?.normalizedRequest?.headers as
4143
| Record<string, string | string[] | undefined>
4244
| undefined;
@@ -45,41 +47,30 @@ export function maybeStartCronCheckIn(span: Span, route: string | undefined): vo
4547
return;
4648
}
4749

48-
// Check if this is a Vercel cron request
4950
const userAgent = Array.isArray(headers['user-agent']) ? headers['user-agent'][0] : headers['user-agent'];
50-
5151
if (!userAgent?.includes('vercel-cron')) {
5252
return;
5353
}
5454

55-
// Find matching cron configuration
5655
const matchedCron = vercelCronsConfig.find(cron => cron.path === route);
57-
5856
if (!matchedCron?.path || !matchedCron.schedule) {
5957
return;
6058
}
6159

6260
const monitorSlug = matchedCron.path;
6361
const startTime = _INTERNAL_safeDateNow() / 1000;
6462

65-
// Start the check-in
6663
const checkInId = captureCheckIn(
64+
{ monitorSlug, status: 'in_progress' },
6765
{
68-
monitorSlug,
69-
status: 'in_progress',
70-
},
71-
{
72-
maxRuntime: 60 * 12, // 12 hours - high arbitrary number since we don't know the actual duration
73-
schedule: {
74-
type: 'crontab',
75-
value: matchedCron.schedule,
76-
},
66+
maxRuntime: 60 * 12,
67+
schedule: { type: 'crontab', value: matchedCron.schedule },
7768
},
7869
);
7970

8071
DEBUG_BUILD && debug.log(`[Cron] Started check-in for "${monitorSlug}" with ID "${checkInId}"`);
8172

82-
// Store check-in data on the span for completion later
73+
// Store marking attributes on the span so we can complete the check-in later
8374
span.setAttribute(ATTR_SENTRY_CRON_CHECK_IN_ID, checkInId);
8475
span.setAttribute(ATTR_SENTRY_CRON_MONITOR_SLUG, monitorSlug);
8576
span.setAttribute(ATTR_SENTRY_CRON_START_TIME, startTime);
@@ -91,7 +82,6 @@ export function maybeStartCronCheckIn(span: Span, route: string | undefined): vo
9182
*/
9283
export function maybeCompleteCronCheckIn(span: Span): void {
9384
const spanData = spanToJSON(span).data;
94-
9585
const checkInId = spanData?.[ATTR_SENTRY_CRON_CHECK_IN_ID];
9686
const monitorSlug = spanData?.[ATTR_SENTRY_CRON_MONITOR_SLUG];
9787
const startTime = spanData?.[ATTR_SENTRY_CRON_START_TIME];
@@ -102,10 +92,6 @@ export function maybeCompleteCronCheckIn(span: Span): void {
10292

10393
const duration = _INTERNAL_safeDateNow() / 1000 - startTime;
10494
const spanStatus = spanToJSON(span).status;
105-
106-
// Determine check-in status based on span status
107-
// Only mark as error if span status is explicitly 'error', otherwise treat as success
108-
// Span status can be 'ok', 'error', or undefined (unset) - undefined means success
10995
const checkInStatus = spanStatus === 'error' ? 'error' : 'ok';
11096

11197
captureCheckIn({
@@ -115,7 +101,7 @@ export function maybeCompleteCronCheckIn(span: Span): void {
115101
duration,
116102
});
117103

118-
// Clean up the cron attributes from the span
104+
// Cleanup marking attributes so they don't pollute user span data
119105
span.setAttribute(ATTR_SENTRY_CRON_CHECK_IN_ID, undefined);
120106
span.setAttribute(ATTR_SENTRY_CRON_MONITOR_SLUG, undefined);
121107
span.setAttribute(ATTR_SENTRY_CRON_START_TIME, undefined);

0 commit comments

Comments
 (0)