diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts index c0b9f246a00c..66e598b5c10f 100644 --- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts +++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts @@ -27,6 +27,31 @@ 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; + + if (tunnelRoute && typeof tunnelRoute === 'string') { + const req: unknown = args[0]; + // Check if the current request matches the tunnel route + if (req instanceof Request) { + const url = new URL(req.url); + const isTunnelRequest = url.pathname.startsWith(tunnelRoute); + + if (isTunnelRequest) { + // Create a simple response that mimics NextResponse.next() so we don't need to import internals here + // which breaks next 13 apps + // 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/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 88050713ec8c..3422c75e7b84 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -107,17 +107,17 @@ function getFinalConfigObject( showedExportModeTunnelWarning = true; // eslint-disable-next-line no-console console.warn( - '[@sentry/nextjs] The Sentry Next.js SDK `tunnelRoute` option will not work in combination with Next.js static exports. The `tunnelRoute` option uses serverside features that cannot be accessed in export mode. If you still want to tunnel Sentry events, set up your own tunnel: https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option', + '[@sentry/nextjs] The Sentry Next.js SDK `tunnelRoute` option will not work in combination with Next.js static exports. The `tunnelRoute` option uses server-side features that cannot be accessed in export mode. If you still want to tunnel Sentry events, set up your own tunnel: https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option', ); } } else { const resolvedTunnelRoute = - typeof userSentryOptions.tunnelRoute === 'boolean' + typeof userSentryOptions.tunnelRoute === 'boolean' && userSentryOptions.tunnelRoute === true ? generateRandomTunnelRoute() : userSentryOptions.tunnelRoute; // Update the global options object to use the resolved value everywhere - userSentryOptions.tunnelRoute = resolvedTunnelRoute; + userSentryOptions.tunnelRoute = resolvedTunnelRoute || undefined; setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute); } } diff --git a/packages/nextjs/test/config/wrappers.test.ts b/packages/nextjs/test/config/wrappers.test.ts index 70d032d2e7b2..c96184df51cf 100644 --- a/packages/nextjs/test/config/wrappers.test.ts +++ b/packages/nextjs/test/config/wrappers.test.ts @@ -2,7 +2,12 @@ import type { Client } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import type { IncomingMessage, ServerResponse } from 'http'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; -import { wrapGetInitialPropsWithSentry, wrapGetServerSidePropsWithSentry } from '../../src/common'; +import { + wrapGetInitialPropsWithSentry, + wrapGetServerSidePropsWithSentry, + wrapMiddlewareWithSentry, +} from '../../src/common'; +import type { EdgeRouteHandler } from '../../src/edge/types'; const startSpanManualSpy = vi.spyOn(SentryCore, 'startSpanManual'); @@ -84,3 +89,102 @@ describe('data-fetching function wrappers should not create manual spans', () => expect(mockSetAttribute).not.toHaveBeenCalled(); }); }); + +describe('wrapMiddlewareWithSentry', () => { + afterEach(() => { + vi.clearAllMocks(); + if ('_sentryRewritesTunnelPath' in globalThis) { + delete (globalThis as any)._sentryRewritesTunnelPath; + } + }); + + 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'); + + 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); + const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); + + const mockRequest = new Request('https://example.com/api/users', { method: 'GET' }); + + const result = await wrappedOriginal(mockRequest); + + expect(origFunction).toHaveBeenCalledWith(mockRequest); + expect(result).toBe(mockReturnValue); + }); + + test('should handle non-Request arguments', async () => { + const mockReturnValue = { status: 200 }; + const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue); + const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); + + const mockArgs = { someProperty: 'value' }; + + const result = await wrappedOriginal(mockArgs); + + expect(origFunction).toHaveBeenCalledWith(mockArgs); + expect(result).toBe(mockReturnValue); + }); + + test('should handle errors in middleware function', async () => { + const testError = new Error('Test middleware error'); + const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => { + throw testError; + }); + const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); + + const mockRequest = new Request('https://example.com/api/users'); + + await expect(wrappedOriginal(mockRequest)).rejects.toThrow('Test middleware error'); + 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); + }); +});