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 numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
// 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.
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 numberDiff line numberDiff 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
4 changes: 4 additions & 0 deletions packages/nextjs/package.json
Original file line numberDiff line numberDiff 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
24 changes: 24 additions & 0 deletions packages/nextjs/src/config/deprecatedWithSentryConfig.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
import { consoleSandbox } from '@sentry/core';
import type { SentryBuildOptions } from './types';
import { withSentryConfig as withSentryConfigImpl } from './withSentryConfig';

let hasWarned = false;

/**
* Deprecation shim for the `withSentryConfig` re-export on the `@sentry/nextjs` entry. Kept separate from
* `./config` so that importing from `@sentry/nextjs/config` stays silent.
*/
export function withSentryConfig<C>(nextConfig?: C, sentryBuildOptions: SentryBuildOptions = {}): C {
if (!hasWarned) {
hasWarned = true;
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.warn(
'[@sentry/nextjs] Importing `withSentryConfig` from `@sentry/nextjs` is deprecated and will stop working in v11. Import it from `@sentry/nextjs/config` instead:\n' +
" import { withSentryConfig } from '@sentry/nextjs/config';",
);
});
}

return withSentryConfigImpl(nextConfig, sentryBuildOptions);
}
3 changes: 2 additions & 1 deletion packages/nextjs/src/index.server.ts
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
export * from './config';
export { withSentryConfig } from './config/deprecatedWithSentryConfig';
export type { SentryBuildOptions } from './config';
export * from './server';
9 changes: 7 additions & 2 deletions packages/nextjs/src/index.types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,10 +8,11 @@
import type { Client, Integration, Options, StackParser } from '@sentry/core';
import type * as clientSdk from './client';
import type { ServerComponentContext, VercelCronsConfig } from './common/types';
import type * as configSdk from './config';
import type * as edgeSdk from './edge';
import type * as serverSdk from './server';

export * from './config';
export type { SentryBuildOptions } from './config';
export * from './client';
export * from './server';
export * from './edge';
Expand DownExpand Up@@ -44,7 +45,11 @@ export declare const withErrorBoundary: typeof clientSdk.withErrorBoundary;

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

export { withSentryConfig } from './config';
/**
* @deprecated Import `withSentryConfig` from `@sentry/nextjs/config` instead. The `@sentry/nextjs` export is removed
* in v11.
*/
export declare const withSentryConfig: typeof configSdk.withSentryConfig;

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

vi.mock('../src/config/withSentryConfig', () => ({
withSentryConfig: vi.fn((nextConfig: unknown) => nextConfig),
}));

/**
* `next.config.mjs` is loaded by a plain Node ESM loader, but build-time config code resolves webpack loader and
* template paths with `__dirname`, 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.
*/
describe('`./config` subpath export', () => {
const packageExports = (
JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf8')) as {
exports: Record<string, unknown>;
}
).exports;

it('resolves to the CJS build for every condition', () => {
expect(packageExports['./config']).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(packageExports['./config'])).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 in a `next.config.mjs`.
*
* 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');
});
});

describe('deprecated `withSentryConfig` on the `@sentry/nextjs` entry', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
});

it('delegates to `@sentry/nextjs/config`', async () => {
const { withSentryConfig } = await import('../src/config/deprecatedWithSentryConfig');
const { withSentryConfig: withSentryConfigImpl } = await import('../src/config/withSentryConfig');
const nextConfig = { reactStrictMode: true };

expect(withSentryConfig(nextConfig, { silent: true })).toBe(nextConfig);
expect(withSentryConfigImpl).toHaveBeenCalledWith(nextConfig, { silent: true });
});

it('warns once, no matter how often the config is materialized', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
const { withSentryConfig } = await import('../src/config/deprecatedWithSentryConfig');

withSentryConfig({});
withSentryConfig({});

expect(warn).toHaveBeenCalledTimes(1);
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("import { withSentryConfig } from '@sentry/nextjs/config'"),
);
});

it('does not warn when imported from `@sentry/nextjs/config`', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
const { withSentryConfig } = await import('../src/config');

withSentryConfig({});

expect(warn).not.toHaveBeenCalled();
});
});
Loading