Skip to content

Commit 4f38b4b

Browse files
authored
fix(nextjs): Add edge polyfills for nextjs-13 in dev mode (#17488)
This issue is only observed for Nextjs 13 and specifically for apps that don't use the app router. Similarly to what we do in vercel-edge, we polyfill `performance`. This is only done for dev mode as it's not an issue otherwise. Note: I tried to reproduce this via our existing e2e test apps but they're all using the app router so it's not manifesting there. Closes: #17343
1 parent 636b2a5 commit 4f38b4b

File tree

10 files changed

+216
-6
lines changed

10 files changed

+216
-6
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { NextResponse } from 'next/server';
2+
3+
export function middleware() {
4+
// Basic middleware to ensure that the build works with edge runtime
5+
return NextResponse.next();
6+
}

dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ test('should create a transaction for a CJS pages router API endpoint', async ({
1111
const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => {
1212
return (
1313
transactionEvent.transaction === 'GET /api/cjs-api-endpoint' &&
14-
transactionEvent.contexts?.trace?.op === 'http.server'
14+
transactionEvent.contexts?.trace?.op === 'http.server' &&
15+
transactionEvent.transaction_info?.source === 'route'
1516
);
1617
});
1718

@@ -73,7 +74,8 @@ test('should not mess up require statements in CJS API endpoints', async ({ requ
7374
const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => {
7475
return (
7576
transactionEvent.transaction === 'GET /api/cjs-api-endpoint-with-require' &&
76-
transactionEvent.contexts?.trace?.op === 'http.server'
77+
transactionEvent.contexts?.trace?.op === 'http.server' &&
78+
transactionEvent.transaction_info?.source === 'route'
7779
);
7880
});
7981

dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ const cases = [
2222
cases.forEach(({ name, url, transactionName }) => {
2323
test(`Should capture transactions for routes with various shapes (${name})`, async ({ request }) => {
2424
const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => {
25-
return transactionEvent.transaction === transactionName && transactionEvent.contexts?.trace?.op === 'http.server';
25+
return (
26+
transactionEvent.transaction === transactionName &&
27+
transactionEvent.contexts?.trace?.op === 'http.server' &&
28+
transactionEvent.transaction_info?.source === 'route'
29+
);
2630
});
2731

2832
request.get(url).catch(() => {

packages/nextjs/.eslintrc.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,11 @@ module.exports = {
2121
'import/no-extraneous-dependencies': 'off',
2222
},
2323
},
24+
{
25+
files: ['src/config/polyfills/perf_hooks.js'],
26+
globals: {
27+
globalThis: 'readonly',
28+
},
29+
},
2430
],
2531
};

packages/nextjs/rollup.npm.config.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,20 @@ export default [
8888
},
8989
}),
9090
),
91+
...makeNPMConfigVariants(
92+
makeBaseNPMConfig({
93+
entrypoints: ['src/config/polyfills/perf_hooks.js'],
94+
95+
packageSpecificConfig: {
96+
output: {
97+
// Preserve the original file structure (i.e., so that everything is still relative to `src`)
98+
entryFileNames: 'config/polyfills/[name].js',
99+
100+
// make it so Rollup calms down about the fact that we're combining default and named exports
101+
exports: 'named',
102+
},
103+
},
104+
}),
105+
),
91106
...makeOtelLoaders('./build', 'sentry-node'),
92107
];
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Polyfill for Node.js perf_hooks module in edge runtime
2+
// This mirrors the polyfill from packages/vercel-edge/rollup.npm.config.mjs
3+
const __sentry__timeOrigin = Date.now();
4+
5+
// Ensure performance global is available
6+
if (typeof globalThis !== 'undefined' && globalThis.performance === undefined) {
7+
globalThis.performance = {
8+
timeOrigin: __sentry__timeOrigin,
9+
now: function () {
10+
return Date.now() - __sentry__timeOrigin;
11+
},
12+
};
13+
}
14+
15+
// Export the performance object for perf_hooks compatibility
16+
export const performance = globalThis.performance || {
17+
timeOrigin: __sentry__timeOrigin,
18+
now: function () {
19+
return Date.now() - __sentry__timeOrigin;
20+
},
21+
};
22+
23+
// Default export for CommonJS compatibility
24+
export default {
25+
performance,
26+
};

packages/nextjs/src/config/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,7 @@ export type BuildContext = {
581581
webpack: {
582582
version: string;
583583
DefinePlugin: new (values: Record<string, string | boolean>) => WebpackPluginInstance;
584+
ProvidePlugin: new (values: Record<string, string | string[]>) => WebpackPluginInstance;
584585
};
585586
// eslint-disable-next-line @typescript-eslint/no-explicit-any
586587
defaultLoaders: any; // needed for type tests (test:types)

packages/nextjs/src/config/webpack.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export function constructWebpackConfigFunction(
6060
const pageExtensions = userNextConfig.pageExtensions || ['tsx', 'ts', 'jsx', 'js'];
6161
const dotPrefixedPageExtensions = pageExtensions.map(ext => `.${ext}`);
6262
const pageExtensionRegex = pageExtensions.map(escapeStringForRegex).join('|');
63+
const nextVersion = nextJsVersion || getNextjsVersion();
64+
const { major } = parseSemver(nextVersion || '');
6365

6466
// We add `.ts` and `.js` back in because `pageExtensions` might not be relevant to the instrumentation file
6567
// e.g. user's setting `.mdx`. In that case we still want to default look up
@@ -70,8 +72,6 @@ export function constructWebpackConfigFunction(
7072
warnAboutDeprecatedConfigFiles(projectDir, instrumentationFile, runtime);
7173
}
7274
if (runtime === 'server') {
73-
const nextJsVersion = getNextjsVersion();
74-
const { major } = parseSemver(nextJsVersion || '');
7575
// was added in v15 (https://github.com/vercel/next.js/pull/67539)
7676
if (major && major >= 15) {
7777
warnAboutMissingOnRequestErrorHandler(instrumentationFile);
@@ -103,6 +103,11 @@ export function constructWebpackConfigFunction(
103103

104104
addOtelWarningIgnoreRule(newConfig);
105105

106+
// Add edge runtime polyfills when building for edge in dev mode
107+
if (major && major === 13 && runtime === 'edge' && isDev) {
108+
addEdgeRuntimePolyfills(newConfig, buildContext);
109+
}
110+
106111
let pagesDirPath: string | undefined;
107112
const maybePagesDirPath = path.join(projectDir, 'pages');
108113
const maybeSrcPagesDirPath = path.join(projectDir, 'src', 'pages');
@@ -865,6 +870,24 @@ function addOtelWarningIgnoreRule(newConfig: WebpackConfigObjectWithModuleRules)
865870
}
866871
}
867872

873+
function addEdgeRuntimePolyfills(newConfig: WebpackConfigObjectWithModuleRules, buildContext: BuildContext): void {
874+
// Use ProvidePlugin to inject performance global only when accessed
875+
newConfig.plugins = newConfig.plugins || [];
876+
newConfig.plugins.push(
877+
new buildContext.webpack.ProvidePlugin({
878+
performance: [path.resolve(__dirname, 'polyfills', 'perf_hooks.js'), 'performance'],
879+
}),
880+
);
881+
882+
// Add module resolution aliases for problematic Node.js modules in edge runtime
883+
newConfig.resolve = newConfig.resolve || {};
884+
newConfig.resolve.alias = {
885+
...newConfig.resolve.alias,
886+
// Redirect perf_hooks imports to a polyfilled version
887+
perf_hooks: path.resolve(__dirname, 'polyfills', 'perf_hooks.js'),
888+
};
889+
}
890+
868891
function _getModules(projectDir: string): Record<string, string> {
869892
try {
870893
const packageJson = path.join(projectDir, 'package.json');

packages/nextjs/test/config/fixtures.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,13 @@ export function getBuildContext(
9999
distDir: '.next',
100100
...materializedNextConfig,
101101
} as NextConfigObject,
102-
webpack: { version: webpackVersion, DefinePlugin: class {} as any },
102+
webpack: {
103+
version: webpackVersion,
104+
DefinePlugin: class {} as any,
105+
ProvidePlugin: class {
106+
constructor(public definitions: Record<string, any>) {}
107+
} as any,
108+
},
103109
defaultLoaders: true,
104110
totalPages: 2,
105111
isServer: buildTarget === 'server' || buildTarget === 'edge',

packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
import '../mocks';
33
import * as core from '@sentry/core';
44
import { describe, expect, it, vi } from 'vitest';
5+
import * as util from '../../../src/config/util';
56
import * as getWebpackPluginOptionsModule from '../../../src/config/webpackPluginOptions';
67
import {
78
CLIENT_SDK_CONFIG_FILE,
89
clientBuildContext,
910
clientWebpackConfig,
11+
edgeBuildContext,
1012
exportedNextConfig,
1113
serverBuildContext,
1214
serverWebpackConfig,
@@ -185,4 +187,123 @@ describe('constructWebpackConfigFunction()', () => {
185187
});
186188
});
187189
});
190+
191+
describe('edge runtime polyfills', () => {
192+
it('adds polyfills only for edge runtime in dev mode on Next.js 13', async () => {
193+
// Mock Next.js version 13 - polyfills should be added
194+
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('13.0.0');
195+
196+
// Test edge runtime in dev mode with Next.js 13 - should add polyfills
197+
const edgeDevBuildContext = { ...edgeBuildContext, dev: true };
198+
const edgeDevConfig = await materializeFinalWebpackConfig({
199+
exportedNextConfig,
200+
incomingWebpackConfig: serverWebpackConfig,
201+
incomingWebpackBuildContext: edgeDevBuildContext,
202+
});
203+
204+
const edgeProvidePlugin = edgeDevConfig.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin');
205+
expect(edgeProvidePlugin).toBeDefined();
206+
expect(edgeDevConfig.resolve?.alias?.perf_hooks).toMatch(/perf_hooks\.js$/);
207+
208+
vi.restoreAllMocks();
209+
});
210+
211+
it('does NOT add polyfills for edge runtime in prod mode even on Next.js 13', async () => {
212+
// Mock Next.js version 13 - but prod mode should still not add polyfills
213+
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('13.0.0');
214+
215+
// Test edge runtime in prod mode - should NOT add polyfills
216+
const edgeProdBuildContext = { ...edgeBuildContext, dev: false };
217+
const edgeProdConfig = await materializeFinalWebpackConfig({
218+
exportedNextConfig,
219+
incomingWebpackConfig: serverWebpackConfig,
220+
incomingWebpackBuildContext: edgeProdBuildContext,
221+
});
222+
223+
const edgeProdProvidePlugin = edgeProdConfig.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin');
224+
expect(edgeProdProvidePlugin).toBeUndefined();
225+
226+
vi.restoreAllMocks();
227+
});
228+
229+
it('does NOT add polyfills for server runtime even on Next.js 13', async () => {
230+
// Mock Next.js version 13
231+
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('13.0.0');
232+
233+
// Test server runtime in dev mode - should NOT add polyfills
234+
const serverDevBuildContext = { ...serverBuildContext, dev: true };
235+
const serverDevConfig = await materializeFinalWebpackConfig({
236+
exportedNextConfig,
237+
incomingWebpackConfig: serverWebpackConfig,
238+
incomingWebpackBuildContext: serverDevBuildContext,
239+
});
240+
241+
const serverProvidePlugin = serverDevConfig.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin');
242+
expect(serverProvidePlugin).toBeUndefined();
243+
244+
vi.restoreAllMocks();
245+
});
246+
247+
it('does NOT add polyfills for client runtime even on Next.js 13', async () => {
248+
// Mock Next.js version 13
249+
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('13.0.0');
250+
251+
// Test client runtime in dev mode - should NOT add polyfills
252+
const clientDevBuildContext = { ...clientBuildContext, dev: true };
253+
const clientDevConfig = await materializeFinalWebpackConfig({
254+
exportedNextConfig,
255+
incomingWebpackConfig: clientWebpackConfig,
256+
incomingWebpackBuildContext: clientDevBuildContext,
257+
});
258+
259+
const clientProvidePlugin = clientDevConfig.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin');
260+
expect(clientProvidePlugin).toBeUndefined();
261+
262+
vi.restoreAllMocks();
263+
});
264+
265+
it('does NOT add polyfills for edge runtime in dev mode on Next.js versions other than 13', async () => {
266+
const edgeDevBuildContext = { ...edgeBuildContext, dev: true };
267+
268+
// Test with Next.js 12 - should NOT add polyfills
269+
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('12.3.0');
270+
const edgeConfigV12 = await materializeFinalWebpackConfig({
271+
exportedNextConfig,
272+
incomingWebpackConfig: serverWebpackConfig,
273+
incomingWebpackBuildContext: edgeDevBuildContext,
274+
});
275+
expect(edgeConfigV12.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin')).toBeUndefined();
276+
vi.restoreAllMocks();
277+
278+
// Test with Next.js 14 - should NOT add polyfills
279+
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('14.0.0');
280+
const edgeConfigV14 = await materializeFinalWebpackConfig({
281+
exportedNextConfig,
282+
incomingWebpackConfig: serverWebpackConfig,
283+
incomingWebpackBuildContext: edgeDevBuildContext,
284+
});
285+
expect(edgeConfigV14.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin')).toBeUndefined();
286+
vi.restoreAllMocks();
287+
288+
// Test with Next.js 15 - should NOT add polyfills
289+
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.0.0');
290+
const edgeConfigV15 = await materializeFinalWebpackConfig({
291+
exportedNextConfig,
292+
incomingWebpackConfig: serverWebpackConfig,
293+
incomingWebpackBuildContext: edgeDevBuildContext,
294+
});
295+
expect(edgeConfigV15.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin')).toBeUndefined();
296+
vi.restoreAllMocks();
297+
298+
// Test with undefined Next.js version - should NOT add polyfills
299+
vi.spyOn(util, 'getNextjsVersion').mockReturnValue(undefined);
300+
const edgeConfigUndefined = await materializeFinalWebpackConfig({
301+
exportedNextConfig,
302+
incomingWebpackConfig: serverWebpackConfig,
303+
incomingWebpackBuildContext: edgeDevBuildContext,
304+
});
305+
expect(edgeConfigUndefined.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin')).toBeUndefined();
306+
vi.restoreAllMocks();
307+
});
308+
});
188309
});

0 commit comments

Comments
 (0)