diff --git a/MIGRATION.md b/MIGRATION.md index f059cd041035..80630a915f83 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1232,6 +1232,8 @@ Because the integration owns error capture, `setupFastifyErrorHandler` no longer **Tracing removed from generated templates:** Tracing was removed from the generated Pages Router API handler, Edge API handler, and Middleware wrapper templates. Route handlers and middleware are still instrumented automatically, so no action is required for most users. +**`tunnelRoute` requests now run through your middleware:** Webpack builds no longer skip your middleware for tunnel route requests. If your middleware blocks unauthenticated requests globally, exclude the tunnel route in its `matcher`, which requires a fixed string `tunnelRoute` instead of `true`. + **Unified `reactComponentAnnotation` option:** React component annotation is now configured through a single top-level `reactComponentAnnotation` option that applies to both webpack and Turbopack builds: ```js diff --git a/packages/nextjs/src/common/utils/tunnelPathnameMatch.ts b/packages/nextjs/src/common/utils/tunnelPathnameMatch.ts index ce8cacd0d264..9f107d33636c 100644 --- a/packages/nextjs/src/common/utils/tunnelPathnameMatch.ts +++ b/packages/nextjs/src/common/utils/tunnelPathnameMatch.ts @@ -6,28 +6,3 @@ export function isPathnameUnderSentryTunnelRoute(pathname: string, tunnelPath: string): boolean { return pathname === tunnelPath || pathname.startsWith(`${tunnelPath}/`); } - -/** - * Returns true only for requests the tunnel rewrite (see `setUpTunnelRewriteRules`) would serve. - * - * This decides whether the user's middleware is skipped, so it must never be broader than the rewrite: - * anything it matches that Next.js does not rewrite to Sentry reaches the app without middleware. - */ -export function isSentryTunnelRequest(request: Request, tunnelPath: string): boolean { - // The SDK transport only ever sends POST requests - if (request.method !== 'POST') { - return false; - } - - const url = new URL(request.url); - - if (url.pathname !== tunnelPath && url.pathname !== `${tunnelPath}/`) { - return false; - } - - // Next.js evaluates `has` conditions against the last value of a repeated query param, so every value has to qualify - return ['o', 'p'].every(key => { - const values = url.searchParams.getAll(key); - return values.length > 0 && values.every(value => /^\d+$/.test(value)); - }); -} diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts index c9367433b123..fb1c56565a4d 100644 --- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts +++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts @@ -9,7 +9,6 @@ import { withIsolationScope, } from '@sentry/core'; import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd'; -import { isSentryTunnelRequest } from '../common/utils/tunnelPathnameMatch'; import type { EdgeRouteHandler } from '../edge/types'; /** @@ -27,26 +26,6 @@ export function wrapMiddlewareWithSentry( ): (...params: Parameters) => Promise> { return new Proxy(middleware, { apply: async (wrappingTarget, thisArg, args: Parameters) => { - const tunnelRoute = - '_sentryRewritesTunnelPath' in globalThis - ? (globalThis as Record)._sentryRewritesTunnelPath - : undefined; - - // TODO: This can never work with Turbopack, need to remove it for consistency between builds. - if (tunnelRoute && typeof tunnelRoute === 'string') { - const req: unknown = args[0]; - if (req instanceof Request && isSentryTunnelRequest(req, tunnelRoute)) { - // Create a simple response that mimics NextResponse.next() so we don't need to import Next.js internals here - // https://github.com/vercel/next.js/blob/c12c9c1f78ad384270902f0890dc4cd341408105/packages/next/src/server/web/spec-extension/response.ts#L146 - return new Response(null, { - status: 200, - headers: { - 'x-middleware-next': '1', - }, - }) as ReturnType; - } - } - // TODO: We still should add central isolation scope creation for when our build-time instrumentation does not work anymore with turbopack. return withIsolationScope(isolationScope => { const req: unknown = args[0]; diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 6750f2d43fff..2ad05c994433 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -268,6 +268,10 @@ export type SentryBuildOptions = Omit< * - Pass `true` to auto-generate a random, ad-blocker-resistant route for each build * - Pass a string path (e.g., '/monitoring') to use a custom route * + * Tunnel requests go through your middleware (`proxy.ts` / `middleware.ts`) like any other request. If your + * middleware redirects or blocks unauthenticated requests, exclude the tunnel route in its `matcher` so events + * can reach Sentry. Matchers have to be static, so use a fixed string route in that case rather than `true`. + * * NOTE: This feature only works with Next.js 11+ */ tunnelRoute?: string | boolean; diff --git a/packages/nextjs/test/config/wrappers.test.ts b/packages/nextjs/test/config/wrappers.test.ts index e2059853ce96..9d121172eea9 100644 --- a/packages/nextjs/test/config/wrappers.test.ts +++ b/packages/nextjs/test/config/wrappers.test.ts @@ -104,23 +104,6 @@ describe('wrapMiddlewareWithSentry', () => { } }); - test('should skip processing and return NextResponse.next() for tunnel route requests', async () => { - // Set up tunnel route in global - (globalThis as any)._sentryRewritesTunnelPath = '/monitoring/tunnel'; - - const origFunction: EdgeRouteHandler = vi.fn(async () => ({ status: 200 })); - const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); - - // Create a mock Request that matches the tunnel route - const mockRequest = new Request('https://example.com/monitoring/tunnel?o=123&p=456', { method: 'POST' }); - - const result = await wrappedOriginal(mockRequest); - - // Should skip calling the original function - expect(origFunction).not.toHaveBeenCalled(); - expect(result).toBeDefined(); - }); - test('should process normal request and call original function', async () => { const mockReturnValue = { status: 200 }; const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue); @@ -160,82 +143,14 @@ describe('wrapMiddlewareWithSentry', () => { expect(origFunction).toHaveBeenCalledWith(mockRequest); }); - test('should not process tunnel route when no tunnel path is set', async () => { - if ('_sentryRewritesTunnelPath' in globalThis) { - delete (globalThis as any)._sentryRewritesTunnelPath; - } - - const mockReturnValue = { status: 200 }; - const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue); - const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); - - const mockRequest = new Request('https://example.com/monitoring/tunnel/sentry?o=123'); - - const result = await wrappedOriginal(mockRequest); - - // Should process normally since no tunnel path is configured - expect(origFunction).toHaveBeenCalledWith(mockRequest); - expect(result).toBe(mockReturnValue); - }); - - test('should process request when tunnel path is set but request does not match', async () => { - (globalThis as any)._sentryRewritesTunnelPath = '/monitoring/tunnel'; - - const mockReturnValue = { status: 200 }; - const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue); - const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); - - const mockRequest = new Request('https://example.com/api/users', { method: 'GET' }); - - const result = await wrappedOriginal(mockRequest); - - // Should process normally since request doesn't match tunnel path - expect(origFunction).toHaveBeenCalledWith(mockRequest); - expect(result).toBe(mockReturnValue); - }); - - test('should not treat paths as tunnel when they only share a prefix with tunnelRoute', async () => { - (globalThis as any)._sentryRewritesTunnelPath = '/api/t'; - - const mockReturnValue = { status: 200 }; - const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue); - const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); - - const mockRequest = new Request('https://example.com/api/things', { method: 'GET' }); - - const result = await wrappedOriginal(mockRequest); - - expect(origFunction).toHaveBeenCalledWith(mockRequest); - expect(result).toBe(mockReturnValue); - }); - - test('should skip processing for the tunnel route with a trailing slash', async () => { - (globalThis as any)._sentryRewritesTunnelPath = '/monitoring'; - - const origFunction: EdgeRouteHandler = vi.fn(async () => ({ status: 200 })); - const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); - - await wrappedOriginal(new Request('https://example.com/monitoring/?o=123&p=456&r=us', { method: 'POST' })); - - expect(origFunction).not.toHaveBeenCalled(); - }); - - test.each([ - ['a sub-path of the tunnel route', 'https://example.com/monitoring/anything/at/all?o=123&p=456', 'POST'], - ['a tunnel request without query params', 'https://example.com/monitoring', 'POST'], - ['a tunnel request without project id', 'https://example.com/monitoring?o=123', 'POST'], - ['a tunnel request with non-numeric ids', 'https://example.com/monitoring?o=abc&p=456', 'POST'], - ['a tunnel request with a repeated non-numeric org id', 'https://example.com/monitoring?o=123&o=abc&p=456', 'POST'], - ['a tunnel request with a repeated empty project id', 'https://example.com/monitoring?o=123&p=456&p=', 'POST'], - ['a non-POST tunnel request', 'https://example.com/monitoring?o=123&p=456', 'GET'], - ])('should run the middleware for %s', async (_, url, method) => { + test('should run the middleware for requests to the tunnel route', async () => { (globalThis as any)._sentryRewritesTunnelPath = '/monitoring'; const mockReturnValue = { status: 200 }; const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue); const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); - const mockRequest = new Request(url, { method }); + const mockRequest = new Request('https://example.com/monitoring?o=123&p=456', { method: 'POST' }); const result = await wrappedOriginal(mockRequest);