From 6af264a35802a1b5305b19a5ec0092f30f1fab1e Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 4 Jul 2025 12:37:58 +0200 Subject: [PATCH 1/3] early return in tunnel requests --- packages/nextjs/rollup.npm.config.mjs | 2 +- .../src/common/wrapMiddlewareWithSentry.ts | 18 ++++++++++++++++++ packages/nextjs/src/config/withSentryConfig.ts | 6 +++--- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/nextjs/rollup.npm.config.mjs b/packages/nextjs/rollup.npm.config.mjs index 3582cf4574ef..a1f0fa6b8842 100644 --- a/packages/nextjs/rollup.npm.config.mjs +++ b/packages/nextjs/rollup.npm.config.mjs @@ -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', 'next/headers', 'next/server', 'stacktrace-parser'], // Next.js and our users are more happy when our client code has the "use client" directive plugins: [ diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts index c0b9f246a00c..7b968c827cc7 100644 --- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts +++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts @@ -13,6 +13,7 @@ import { winterCGRequestToRequestData, withIsolationScope, } from '@sentry/core'; +import { NextResponse } from 'next/server'; import type { EdgeRouteHandler } from '../edge/types'; import { flushSafelyWithTimeout } from './utils/responseEnd'; @@ -27,6 +28,23 @@ 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) { + return NextResponse.next() 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); } } From 8823100f01a5b9abfbf883d63c3e8c251d1c276c Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 4 Jul 2025 13:01:19 +0200 Subject: [PATCH 2/3] add some unit tests --- packages/nextjs/test/config/wrappers.test.ts | 106 ++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) 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); + }); +}); From 475d448675f8e905e1e6e4f75bd33da3ca16c724 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 4 Jul 2025 15:05:21 +0200 Subject: [PATCH 3/3] mimic next response --- packages/nextjs/rollup.npm.config.mjs | 2 +- .../nextjs/src/common/wrapMiddlewareWithSentry.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/nextjs/rollup.npm.config.mjs b/packages/nextjs/rollup.npm.config.mjs index a1f0fa6b8842..3582cf4574ef 100644 --- a/packages/nextjs/rollup.npm.config.mjs +++ b/packages/nextjs/rollup.npm.config.mjs @@ -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', 'next/server', 'stacktrace-parser'], + external: ['next/router', 'next/constants', 'next/headers', 'stacktrace-parser'], // Next.js and our users are more happy when our client code has the "use client" directive plugins: [ diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts index 7b968c827cc7..66e598b5c10f 100644 --- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts +++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts @@ -13,7 +13,6 @@ import { winterCGRequestToRequestData, withIsolationScope, } from '@sentry/core'; -import { NextResponse } from 'next/server'; import type { EdgeRouteHandler } from '../edge/types'; import { flushSafelyWithTimeout } from './utils/responseEnd'; @@ -41,7 +40,15 @@ export function wrapMiddlewareWithSentry( const isTunnelRequest = url.pathname.startsWith(tunnelRoute); if (isTunnelRequest) { - return NextResponse.next() as ReturnType; + // 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; } } }