Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { NextResponse } from 'next/server';

export function middleware() {
// Basic middleware to ensure that the build works with edge runtime
return NextResponse.next();
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ test('should create a transaction for a CJS pages router API endpoint', async ({
const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => {
return (
transactionEvent.transaction === 'GET /api/cjs-api-endpoint' &&
transactionEvent.contexts?.trace?.op === 'http.server'
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction_info?.source === 'route'
);
});

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ const cases = [
cases.forEach(({ name, url, transactionName }) => {
test(`Should capture transactions for routes with various shapes (${name})`, async ({ request }) => {
const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => {
return transactionEvent.transaction === transactionName && transactionEvent.contexts?.trace?.op === 'http.server';
return (
transactionEvent.transaction === transactionName &&
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction_info?.source === 'route'
);
});

request.get(url).catch(() => {
Expand Down
6 changes: 6 additions & 0 deletions packages/nextjs/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,11 @@ module.exports = {
'import/no-extraneous-dependencies': 'off',
},
},
{
files: ['src/config/polyfills/perf_hooks.js'],
globals: {
globalThis: 'readonly',
},
},
],
};
15 changes: 15 additions & 0 deletions packages/nextjs/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,20 @@ export default [
},
}),
),
...makeNPMConfigVariants(
makeBaseNPMConfig({
entrypoints: ['src/config/polyfills/perf_hooks.js'],

packageSpecificConfig: {
output: {
// Preserve the original file structure (i.e., so that everything is still relative to `src`)
entryFileNames: 'config/polyfills/[name].js',

// make it so Rollup calms down about the fact that we're combining default and named exports
exports: 'named',
},
},
}),
),
...makeOtelLoaders('./build', 'sentry-node'),
];
26 changes: 26 additions & 0 deletions packages/nextjs/src/config/polyfills/perf_hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Polyfill for Node.js perf_hooks module in edge runtime
// This mirrors the polyfill from packages/vercel-edge/rollup.npm.config.mjs
const __sentry__timeOrigin = Date.now();

// Ensure performance global is available
if (typeof globalThis !== 'undefined' && globalThis.performance === undefined) {
globalThis.performance = {
timeOrigin: __sentry__timeOrigin,
now: function () {
return Date.now() - __sentry__timeOrigin;
},
};
}

// Export the performance object for perf_hooks compatibility
export const performance = globalThis.performance || {
timeOrigin: __sentry__timeOrigin,
now: function () {
return Date.now() - __sentry__timeOrigin;
},
};

// Default export for CommonJS compatibility
export default {
performance,
};
1 change: 1 addition & 0 deletions packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ export type BuildContext = {
webpack: {
version: string;
DefinePlugin: new (values: Record<string, string | boolean>) => WebpackPluginInstance;
ProvidePlugin: new (values: Record<string, string | string[]>) => WebpackPluginInstance;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
defaultLoaders: any; // needed for type tests (test:types)
Expand Down
27 changes: 25 additions & 2 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export function constructWebpackConfigFunction(
const pageExtensions = userNextConfig.pageExtensions || ['tsx', 'ts', 'jsx', 'js'];
const dotPrefixedPageExtensions = pageExtensions.map(ext => `.${ext}`);
const pageExtensionRegex = pageExtensions.map(escapeStringForRegex).join('|');
const nextVersion = nextJsVersion || getNextjsVersion();
const { major } = parseSemver(nextVersion || '');

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

addOtelWarningIgnoreRule(newConfig);

// Add edge runtime polyfills when building for edge in dev mode
if (major && major === 13 && runtime === 'edge' && isDev) {
addEdgeRuntimePolyfills(newConfig, buildContext);
}

let pagesDirPath: string | undefined;
const maybePagesDirPath = path.join(projectDir, 'pages');
const maybeSrcPagesDirPath = path.join(projectDir, 'src', 'pages');
Expand Down Expand Up @@ -865,6 +870,24 @@ function addOtelWarningIgnoreRule(newConfig: WebpackConfigObjectWithModuleRules)
}
}

function addEdgeRuntimePolyfills(newConfig: WebpackConfigObjectWithModuleRules, buildContext: BuildContext): void {
// Use ProvidePlugin to inject performance global only when accessed
newConfig.plugins = newConfig.plugins || [];
newConfig.plugins.push(
new buildContext.webpack.ProvidePlugin({
performance: [path.resolve(__dirname, 'polyfills', 'perf_hooks.js'), 'performance'],
}),
);

// Add module resolution aliases for problematic Node.js modules in edge runtime
newConfig.resolve = newConfig.resolve || {};
newConfig.resolve.alias = {
...newConfig.resolve.alias,
// Redirect perf_hooks imports to a polyfilled version
perf_hooks: path.resolve(__dirname, 'polyfills', 'perf_hooks.js'),
};
}

function _getModules(projectDir: string): Record<string, string> {
try {
const packageJson = path.join(projectDir, 'package.json');
Expand Down
8 changes: 7 additions & 1 deletion packages/nextjs/test/config/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,13 @@ export function getBuildContext(
distDir: '.next',
...materializedNextConfig,
} as NextConfigObject,
webpack: { version: webpackVersion, DefinePlugin: class {} as any },
webpack: {
version: webpackVersion,
DefinePlugin: class {} as any,
ProvidePlugin: class {
constructor(public definitions: Record<string, any>) {}
} as any,
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Can we use a more precise type? or type this with unknown instead of any please?

Copy link
Member Author

Choose a reason for hiding this comment

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

These are just test fixtures, I don't think it's necessary to be more precise here.

},
defaultLoaders: true,
totalPages: 2,
isServer: buildTarget === 'server' || buildTarget === 'edge',
Expand Down
121 changes: 121 additions & 0 deletions packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
import '../mocks';
import * as core from '@sentry/core';
import { describe, expect, it, vi } from 'vitest';
import * as util from '../../../src/config/util';
import * as getWebpackPluginOptionsModule from '../../../src/config/webpackPluginOptions';
import {
CLIENT_SDK_CONFIG_FILE,
clientBuildContext,
clientWebpackConfig,
edgeBuildContext,
exportedNextConfig,
serverBuildContext,
serverWebpackConfig,
Expand Down Expand Up @@ -185,4 +187,123 @@ describe('constructWebpackConfigFunction()', () => {
});
});
});

describe('edge runtime polyfills', () => {
it('adds polyfills only for edge runtime in dev mode on Next.js 13', async () => {
// Mock Next.js version 13 - polyfills should be added
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('13.0.0');

// Test edge runtime in dev mode with Next.js 13 - should add polyfills
const edgeDevBuildContext = { ...edgeBuildContext, dev: true };
const edgeDevConfig = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: edgeDevBuildContext,
});

const edgeProvidePlugin = edgeDevConfig.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin');
expect(edgeProvidePlugin).toBeDefined();
expect(edgeDevConfig.resolve?.alias?.perf_hooks).toMatch(/perf_hooks\.js$/);

vi.restoreAllMocks();
});

it('does NOT add polyfills for edge runtime in prod mode even on Next.js 13', async () => {
// Mock Next.js version 13 - but prod mode should still not add polyfills
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('13.0.0');

// Test edge runtime in prod mode - should NOT add polyfills
const edgeProdBuildContext = { ...edgeBuildContext, dev: false };
const edgeProdConfig = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: edgeProdBuildContext,
});

const edgeProdProvidePlugin = edgeProdConfig.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin');
expect(edgeProdProvidePlugin).toBeUndefined();

vi.restoreAllMocks();
});

it('does NOT add polyfills for server runtime even on Next.js 13', async () => {
// Mock Next.js version 13
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('13.0.0');

// Test server runtime in dev mode - should NOT add polyfills
const serverDevBuildContext = { ...serverBuildContext, dev: true };
const serverDevConfig = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: serverDevBuildContext,
});

const serverProvidePlugin = serverDevConfig.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin');
expect(serverProvidePlugin).toBeUndefined();

vi.restoreAllMocks();
});

it('does NOT add polyfills for client runtime even on Next.js 13', async () => {
// Mock Next.js version 13
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('13.0.0');

// Test client runtime in dev mode - should NOT add polyfills
const clientDevBuildContext = { ...clientBuildContext, dev: true };
const clientDevConfig = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: clientWebpackConfig,
incomingWebpackBuildContext: clientDevBuildContext,
});

const clientProvidePlugin = clientDevConfig.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin');
expect(clientProvidePlugin).toBeUndefined();

vi.restoreAllMocks();
});

it('does NOT add polyfills for edge runtime in dev mode on Next.js versions other than 13', async () => {
const edgeDevBuildContext = { ...edgeBuildContext, dev: true };

// Test with Next.js 12 - should NOT add polyfills
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('12.3.0');
const edgeConfigV12 = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: edgeDevBuildContext,
});
expect(edgeConfigV12.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin')).toBeUndefined();
vi.restoreAllMocks();

// Test with Next.js 14 - should NOT add polyfills
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('14.0.0');
const edgeConfigV14 = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: edgeDevBuildContext,
});
expect(edgeConfigV14.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin')).toBeUndefined();
vi.restoreAllMocks();

// Test with Next.js 15 - should NOT add polyfills
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.0.0');
const edgeConfigV15 = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: edgeDevBuildContext,
});
expect(edgeConfigV15.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin')).toBeUndefined();
vi.restoreAllMocks();

// Test with undefined Next.js version - should NOT add polyfills
vi.spyOn(util, 'getNextjsVersion').mockReturnValue(undefined);
const edgeConfigUndefined = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: edgeDevBuildContext,
});
expect(edgeConfigUndefined.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin')).toBeUndefined();
vi.restoreAllMocks();
});
});
});
Loading