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
15 changes: 15 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,21 @@ The legacy per-transaction profiling sampling options were removed. Configure se

### `@sentry/nextjs`

`withSentryConfig` and the `SentryBuildOptions` type moved to the `@sentry/nextjs/config` entry point and are no
longer exported from `@sentry/nextjs`:

```js
// next.config.mjs

// before
import { withSentryConfig } from '@sentry/nextjs';

// after
import { withSentryConfig } from '@sentry/nextjs/config';
```

The no-op `withSentryConfig` passthroughs that the client and edge builds exported were removed along with it.

The following long-deprecated top-level options in `withSentryConfig` / the `sentry` config were removed. Most of them
moved under the `webpack` option in v10; use the replacement listed below instead:

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { withSentryConfig } = require('@sentry/nextjs');
const { withSentryConfig } = require('@sentry/nextjs/config');

/** @type {import('next').NextConfig} */
const nextConfig = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { withSentryConfig } = require('@sentry/nextjs');
const { withSentryConfig } = require('@sentry/nextjs/config');

/** @type {import('next').NextConfig} */
const nextConfig = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { withSentryConfig } = require('@sentry/nextjs');
const { withSentryConfig } = require('@sentry/nextjs/config');
const createNextIntlPlugin = require('next-intl/plugin');

const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ await import('./src/env.js');
/** @type {import("next").NextConfig} */
const config = {};

import { withSentryConfig } from '@sentry/nextjs';
import { withSentryConfig } from '@sentry/nextjs/config';

export default withSentryConfig(config, {
webpack: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { withSentryConfig } = require('@sentry/nextjs');
const { withSentryConfig } = require('@sentry/nextjs/config');

// Simulate Vercel environment for cron monitoring tests
process.env.VERCEL = '1';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withSentryConfig } from '@sentry/nextjs';
import { withSentryConfig } from '@sentry/nextjs/config';
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withSentryConfig } from '@sentry/nextjs';
import { withSentryConfig } from '@sentry/nextjs/config';
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withSentryConfig } from '@sentry/nextjs';
import { withSentryConfig } from '@sentry/nextjs/config';
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withSentryConfig } from '@sentry/nextjs';
import { withSentryConfig } from '@sentry/nextjs/config';
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withSentryConfig } from '@sentry/nextjs';
import { withSentryConfig } from '@sentry/nextjs/config';
import type { NextConfig } from 'next';

// Simulate Vercel environment for cron monitoring tests
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Deliberately `.mjs`: Next loads it through Node's own ESM loader rather than compiling it, which is the only
// config format that exercises `@sentry/nextjs/config` as a plain-Node ESM consumer.
// See https://github.com/getsentry/sentry-javascript/issues/22791
import { withSentryConfig } from '@sentry/nextjs/config';

/** @type {import('next').NextConfig} */
const nextConfig = {
trailingSlash: true,
};

export default withSentryConfig(nextConfig, {
silent: true,
});

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

// These tests verify that pageload transactions are correctly named when
// trailingSlash: true is enabled in next.config.ts, even when a catch-all
// trailingSlash: true is enabled in next.config.mjs, even when a catch-all
// route exists. See: https://github.com/getsentry/sentry-javascript/issues/19241

test('should create a correctly named pageload transaction for a static route', async ({ page }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withSentryConfig } from '@sentry/nextjs';
import { withSentryConfig } from '@sentry/nextjs/config';
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { withSentryConfig } = require('@sentry/nextjs');
const { withSentryConfig } = require('@sentry/nextjs/config');

/** @type {import('next').NextConfig} */
const nextConfig = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withSentryConfig } from '@sentry/nextjs';
import { withSentryConfig } from '@sentry/nextjs/config';
import type { NextConfig } from 'next';

// Simulate Vercel environment for cron monitoring tests
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { withSentryConfig } = require('@sentry/nextjs');
const { withSentryConfig } = require('@sentry/nextjs/config');

/** @type {import('next').NextConfig} */
const nextConfig = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @type {import("next").NextConfig} */
const config = {};

import { withSentryConfig } from '@sentry/nextjs';
import { withSentryConfig } from '@sentry/nextjs/config';

export default withSentryConfig(config, {
webpack: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withSentryConfig } from '@sentry/nextjs';
import { withSentryConfig } from '@sentry/nextjs/config';
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { withSentryConfig } = require('@sentry/nextjs');
const { withSentryConfig } = require('@sentry/nextjs/config');

/** @type {import('next').NextConfig} */
const nextConfig = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { withSentryConfig } = require('@sentry/nextjs');
const { withSentryConfig } = require('@sentry/nextjs/config');

/** @type {import('next').NextConfig} */
const nextConfig = {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module.exports = nextConfig;

// Injected content via Sentry wizard below

const { withSentryConfig } = require('@sentry/nextjs');
const { withSentryConfig } = require('@sentry/nextjs/config');

module.exports = withSentryConfig(module.exports, {
// For all available options, see:
Expand Down
4 changes: 4 additions & 0 deletions packages/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
"node": "./build/cjs/index.server.js",
"import": "./build/esm/index.server.js"
},
"./config": {
"types": "./build/types/config/index.d.ts",
"default": "./build/cjs/config/index.js"
},
"./async-storage-shim": {
"import": {
"default": "./build/esm/config/templates/requestAsyncStorageShim.js"
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default [
// prevent this internal nextjs code from ending up in our built package (this doesn't happen automatically because
// the name doesn't match an SDK dependency)
packageSpecificConfig: {
external: ['next/router', 'next/constants', 'next/headers', 'stacktrace-parser'],
external: ['next/router', 'next/constants.js', 'next/headers', 'stacktrace-parser'],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

q: why do we need the ending now?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This basically belongs to the part where we split the node exports into require and import (which I took out of this pr). There's no export map from nextjs and the esm import broke here, just left it as a safer default

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

But currently this does not do anything on webpack/turbopack


// Next.js and our users are more happy when our client code has the "use client" directive
plugins: [
Expand Down
7 changes: 0 additions & 7 deletions packages/nextjs/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,3 @@ function getDefaultIntegrations(options: BrowserOptions): Integration[] {

return customDefaultIntegrations;
}

/**
* Just a passthrough in case this is imported from the client.
*/
export function withSentryConfig<T>(exportedUserNextConfig: T): T {
return exportedUserNextConfig;
}
2 changes: 1 addition & 1 deletion packages/nextjs/src/common/utils/isBuild.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PHASE_PRODUCTION_BUILD } from 'next/constants';
import { PHASE_PRODUCTION_BUILD } from 'next/constants.js';

/**
* Decide if the currently running process is part of the build phase or happening at runtime.
Expand Down
7 changes: 0 additions & 7 deletions packages/nextjs/src/edge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,3 @@ export function init(options: VercelEdgeOptions = {}): void {
// The statement above can throw because process is not defined on the client
}
}

/**
* Just a passthrough in case this is imported from the client.
*/
export function withSentryConfig<T>(exportedUserNextConfig: T): T {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

q: why do we no longer need the shims here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Because it's not exported anymore from the runtime subpath

return exportedUserNextConfig;
}
1 change: 0 additions & 1 deletion packages/nextjs/src/index.server.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './config';
export * from './server';
3 changes: 0 additions & 3 deletions packages/nextjs/src/index.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type { ServerComponentContext, VercelCronsConfig } from './common/types';
import type * as edgeSdk from './edge';
import type * as serverSdk from './server';

export * from './config';
export * from './client';
export * from './server';
export * from './edge';
Expand Down Expand Up @@ -52,8 +51,6 @@ export declare const withErrorBoundary: typeof clientSdk.withErrorBoundary;

export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger;

export { withSentryConfig } from './config';

/**
* Wraps a Next.js Pages Router API route with Sentry error and performance instrumentation.
*
Expand Down
115 changes: 115 additions & 0 deletions packages/nextjs/test/configExports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
import { init, parse } from 'cjs-module-lexer';
import { beforeAll, describe, expect, it } from 'vitest';

const nodeRequire = createRequire(import.meta.url);
const packageExports = (nodeRequire('../package.json') as { exports: Record<string, unknown> }).exports;

/**
* `next.config.mjs` is loaded by a plain Node ESM loader, so build-time config code has to work there. The ESM
* variant of it does not: `build/esm/config/**` relies on `__dirname` to resolve loader and template paths, which
* is a `ReferenceError` in an ES module. So `./config` deliberately serves the CJS build to ESM importers too,
* rather than splitting `import`/`require` like the runtime entries do.
*
* There is no dual-package hazard here because this code runs at build time only and holds no SDK state.
*
* Separating the two entry points is also what unblocks serving real ESM to Node consumers later — see
* https://github.com/getsentry/sentry-javascript/issues/22791
*/
describe('`./config` subpath export', () => {
const configExport = packageExports['./config'];

it('resolves to the CJS build for every condition', () => {
expect(configExport).toEqual({
types: './build/types/config/index.d.ts',
default: './build/cjs/config/index.js',
});
});

it('never points a condition at the ESM config build', () => {
expect(JSON.stringify(configExport)).not.toContain('build/esm');
});
});

/**
* ESM consumers of a CJS file only get the named exports `cjs-module-lexer` can see statically — anything it misses
* links as `undefined`. So `withSentryConfig` has to stay statically detectable for `import { withSentryConfig } from
* '@sentry/nextjs/config'` to work.
*
* Exercises the generated artifact, so it needs the package built.
*/
describe('`./config` static exports (generated)', () => {
let staticExports: string[];

beforeAll(async () => {
await init();
staticExports = parse(readFileSync(resolve(__dirname, '../build/cjs/config/index.js'), 'utf8')).exports;
});

it('statically exports `withSentryConfig`', () => {
expect(staticExports).toContain('withSentryConfig');
});
});

/**
* The `node` condition can only ever serve ESM if the server runtime entry stays free of build-time config code, so
* guard that here even though `node` still resolves to CommonJS today. A plain `__dirname` reference is the tripwire:
* it survives bundling, only throws once the enclosing function runs (so merely importing the entry would not catch
* it), and every known offender in `src/config` uses one.
*
* Exercises the generated artifact, so it needs the package built.
*/
describe('ESM server build is loadable under a plain Node loader (generated)', () => {
const entry = resolve(__dirname, '../build/esm/index.server.js');

/** Every file reachable from `entry` via relative specifiers — i.e. this package's own ESM output. */
function collectModuleGraph(from: string, seen = new Set<string>()): Set<string> {
if (seen.has(from)) {
return seen;
}
seen.add(from);

const source = readFileSync(from, 'utf8');
for (const [, specifier] of source.matchAll(/from\s*'(\.[^']+)'|import\s*'(\.[^']+)'/g)) {
if (specifier) {
collectModuleGraph(resolve(dirname(from), specifier), seen);
}
}

return seen;
}

it('reaches no module that references `__dirname`', () => {
const offenders = [...collectModuleGraph(entry)].filter(file =>
/(^|[^.\w])__dirname([^\w]|$)/.test(readFileSync(file, 'utf8')),
);

expect(offenders).toEqual([]);
});

// Catches what the `__dirname` scan cannot: extensionless bare specifiers and require-cycles, which fail at link
// time rather than when some function runs. Needs a real Node loader, hence the child process.
it('imports cleanly, with the full namespace and without `withSentryConfig`', () => {
const script = `
import * as Sentry from ${JSON.stringify(entry)};

const missing = ['init', 'captureException', 'captureMessage', 'setTag', 'addBreadcrumb', 'isEnabled']
.filter(name => typeof Sentry[name] !== 'function');
if (missing.length) {
throw new Error('missing exports: ' + missing.join(', '));
}

// Build-time only — it lives on \`@sentry/nextjs/config\` and must stay out of the runtime graph.
if ('withSentryConfig' in Sentry) {
throw new Error('the runtime entry still exports withSentryConfig');
}
`;

expect(() =>
execFileSync(process.execPath, ['--input-type=module', '-e', script], { stdio: 'pipe' }),
).not.toThrow();
});
});
Loading