From 1d58cd6aaa301aa5169e17a7a504ad7cbc62486f Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 19 Nov 2025 18:41:14 +0200 Subject: [PATCH 01/16] fix: esnure random path is preserved through env --- .../turbopack/constructTurbopackConfig.ts | 8 ++ .../turbopack/generateValueInjectionRules.ts | 7 ++ .../nextjs/src/config/withSentryConfig.ts | 29 +++++- .../test/config/tunnelRouteCaching.test.ts | 91 +++++++++++++++++++ 4 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 packages/nextjs/test/config/tunnelRouteCaching.test.ts diff --git a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts index e46d3f6bb5c7..45d7d541fb14 100644 --- a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts +++ b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts @@ -34,9 +34,17 @@ export function constructTurbopackConfig({ ...(shouldEnableNativeDebugIds ? { debugIds: true } : {}), }; + const tunnelPath = + userSentryOptions.tunnelRoute !== undefined && + userNextConfig.output !== 'export' && + typeof userSentryOptions.tunnelRoute === 'string' + ? `${userNextConfig.basePath ?? ''}${userSentryOptions.tunnelRoute}` + : undefined; + const valueInjectionRules = generateValueInjectionRules({ routeManifest, nextJsVersion, + tunnelPath, }); for (const { matcher, rule } of valueInjectionRules) { diff --git a/packages/nextjs/src/config/turbopack/generateValueInjectionRules.ts b/packages/nextjs/src/config/turbopack/generateValueInjectionRules.ts index 58cf7cdd0a15..2cf96b5f5ad7 100644 --- a/packages/nextjs/src/config/turbopack/generateValueInjectionRules.ts +++ b/packages/nextjs/src/config/turbopack/generateValueInjectionRules.ts @@ -8,9 +8,11 @@ import type { JSONValue, TurbopackMatcherWithRule } from '../types'; export function generateValueInjectionRules({ routeManifest, nextJsVersion, + tunnelPath, }: { routeManifest?: RouteManifest; nextJsVersion?: string; + tunnelPath?: string; }): TurbopackMatcherWithRule[] { const rules: TurbopackMatcherWithRule[] = []; const isomorphicValues: Record = {}; @@ -26,6 +28,11 @@ export function generateValueInjectionRules({ clientValues._sentryRouteManifest = JSON.stringify(routeManifest); } + // Inject tunnel route path for both client and server + if (tunnelPath) { + isomorphicValues._sentryRewritesTunnelPath = tunnelPath; + } + if (Object.keys(isomorphicValues).length > 0) { clientValues = { ...clientValues, ...isomorphicValues }; serverValues = { ...serverValues, ...isomorphicValues }; diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 7ac61d73aa73..4b301aa12017 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -23,6 +23,7 @@ import { supportsProductionCompileHook, } from './util'; import { constructWebpackConfigFunction } from './webpack'; +import { isBuild } from '../common/utils/isBuild'; let showedExportModeTunnelWarning = false; let showedExperimentalBuildModeWarning = false; @@ -121,11 +122,10 @@ function getFinalConfigObject( ); } } else { - const resolvedTunnelRoute = - userSentryOptions.tunnelRoute === true ? generateRandomTunnelRoute() : userSentryOptions.tunnelRoute; - // Update the global options object to use the resolved value everywhere + const resolvedTunnelRoute = resolveTunnelRoute(userSentryOptions.tunnelRoute); userSentryOptions.tunnelRoute = resolvedTunnelRoute || undefined; + setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute); } } @@ -550,3 +550,26 @@ function getInstrumentationClientFileContents(): string | void { } } } + +/** + * Resolves the tunnel route based on the user's configuration and the environment. + * @param tunnelRoute - The user-provided tunnel route option + */ +function resolveTunnelRoute(tunnelRoute: string | true): string { + if (process.env.__SENTRY_TUNNEL_ROUTE__) { + // Reuse cached value from previous build (server/client) + return process.env.__SENTRY_TUNNEL_ROUTE__; + } + + const resolvedTunnelRoute = typeof tunnelRoute === 'string' ? tunnelRoute : generateRandomTunnelRoute(); + + // Cache for subsequent builds (only during build time) + // Turbopack runs the config twice, so we need a shared context to avoid generating a new tunnel route for each build. + // env works well here + // https://linear.app/getsentry/issue/JS-549/adblock-plus-blocking-requests-to-sentry-and-monitoring-tunnel + if (resolvedTunnelRoute) { + process.env.__SENTRY_TUNNEL_ROUTE__ = resolvedTunnelRoute; + } + + return resolvedTunnelRoute; +} diff --git a/packages/nextjs/test/config/tunnelRouteCaching.test.ts b/packages/nextjs/test/config/tunnelRouteCaching.test.ts new file mode 100644 index 000000000000..cf78963ebea1 --- /dev/null +++ b/packages/nextjs/test/config/tunnelRouteCaching.test.ts @@ -0,0 +1,91 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +describe('Tunnel Route Caching (Environment Variable)', () => { + let originalNextPhase: string | undefined; + let originalTunnelRoute: string | undefined; + + beforeEach(() => { + // Save and clear env vars + originalNextPhase = process.env.NEXT_PHASE; + originalTunnelRoute = process.env.__SENTRY_TUNNEL_ROUTE__; + delete process.env.__SENTRY_TUNNEL_ROUTE__; + }); + + afterEach(() => { + // Restore env vars + if (originalNextPhase !== undefined) { + process.env.NEXT_PHASE = originalNextPhase; + } else { + delete process.env.NEXT_PHASE; + } + + if (originalTunnelRoute !== undefined) { + process.env.__SENTRY_TUNNEL_ROUTE__ = originalTunnelRoute; + } else { + delete process.env.__SENTRY_TUNNEL_ROUTE__; + } + }); + + it('caches tunnel route in environment variable during build phase', () => { + process.env.NEXT_PHASE = 'phase-production-build'; + process.env.__SENTRY_TUNNEL_ROUTE__ = '/cached-route-123'; + + // The env var should be accessible + expect(process.env.__SENTRY_TUNNEL_ROUTE__).toBe('/cached-route-123'); + }); + + it('environment variable persists across different contexts', () => { + process.env.NEXT_PHASE = 'phase-production-build'; + process.env.__SENTRY_TUNNEL_ROUTE__ = '/test-route-456'; + + // Simulate accessing from different module + const cachedRoute = process.env.__SENTRY_TUNNEL_ROUTE__; + + expect(cachedRoute).toBe('/test-route-456'); + }); + + it('verifies NEXT_PHASE detection for build time', () => { + process.env.NEXT_PHASE = 'phase-production-build'; + + const isBuildTime = process.env.NEXT_PHASE === 'phase-production-build'; + + expect(isBuildTime).toBe(true); + }); + + it('verifies NEXT_PHASE detection for non-build time', () => { + process.env.NEXT_PHASE = 'phase-development-server'; + + const isBuildTime = process.env.NEXT_PHASE === 'phase-production-build'; + + expect(isBuildTime).toBe(false); + }); + + it('handles missing NEXT_PHASE', () => { + delete process.env.NEXT_PHASE; + + const isBuildTime = process.env.NEXT_PHASE === 'phase-production-build'; + + expect(isBuildTime).toBe(false); + }); +}); + +describe('Random Tunnel Route Generation', () => { + it('generates an 8-character alphanumeric string', () => { + const randomString = Math.random().toString(36).substring(2, 10); + const tunnelRoute = `/${randomString}`; + + // Should be a path with 8 alphanumeric chars + expect(tunnelRoute).toMatch(/^\/[a-z0-9]{8}$/); + }); + + it('generates different values on multiple calls', () => { + const route1 = `/${Math.random().toString(36).substring(2, 10)}`; + const route2 = `/${Math.random().toString(36).substring(2, 10)}`; + + // Very unlikely to be the same (but not impossible) + // This is more of a sanity check + expect(route1).toMatch(/^\/[a-z0-9]{8}$/); + expect(route2).toMatch(/^\/[a-z0-9]{8}$/); + }); +}); + From 73d02757630112336a16b5163dac869d4d536361 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 20 Nov 2025 11:38:06 +0200 Subject: [PATCH 02/16] fix: lint issues --- packages/nextjs/src/config/withSentryConfig.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 4b301aa12017..43455f06f54d 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -23,7 +23,6 @@ import { supportsProductionCompileHook, } from './util'; import { constructWebpackConfigFunction } from './webpack'; -import { isBuild } from '../common/utils/isBuild'; let showedExportModeTunnelWarning = false; let showedExperimentalBuildModeWarning = false; From 2f1de29a5a59c0156f8f0ef5adc41089dd1ce4aa Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 20 Nov 2025 11:39:19 +0200 Subject: [PATCH 03/16] fix: formatting --- packages/nextjs/test/config/tunnelRouteCaching.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nextjs/test/config/tunnelRouteCaching.test.ts b/packages/nextjs/test/config/tunnelRouteCaching.test.ts index cf78963ebea1..4c5fc300c290 100644 --- a/packages/nextjs/test/config/tunnelRouteCaching.test.ts +++ b/packages/nextjs/test/config/tunnelRouteCaching.test.ts @@ -88,4 +88,3 @@ describe('Random Tunnel Route Generation', () => { expect(route2).toMatch(/^\/[a-z0-9]{8}$/); }); }); - From fab8e4bfcd350ee244eecd06cb56a9eeaae0477b Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 20 Nov 2025 12:00:39 +0200 Subject: [PATCH 04/16] fix: turbopack config access --- .../nextjs/src/config/turbopack/constructTurbopackConfig.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts index 45d7d541fb14..b96b8e7f77ee 100644 --- a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts +++ b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts @@ -20,14 +20,14 @@ export function constructTurbopackConfig({ nextJsVersion, }: { userNextConfig: NextConfigObject; - userSentryOptions: SentryBuildOptions; + userSentryOptions?: SentryBuildOptions; routeManifest?: RouteManifest; nextJsVersion?: string; }): TurbopackOptions { // If sourcemaps are disabled, we don't need to enable native debug ids as this will add build time. const shouldEnableNativeDebugIds = (supportsNativeDebugIds(nextJsVersion ?? '') && userNextConfig?.turbopack?.debugIds) ?? - userSentryOptions.sourcemaps?.disable !== true; + userSentryOptions?.sourcemaps?.disable !== true; const newConfig: TurbopackOptions = { ...userNextConfig.turbopack, @@ -35,7 +35,7 @@ export function constructTurbopackConfig({ }; const tunnelPath = - userSentryOptions.tunnelRoute !== undefined && + userSentryOptions?.tunnelRoute !== undefined && userNextConfig.output !== 'export' && typeof userSentryOptions.tunnelRoute === 'string' ? `${userNextConfig.basePath ?? ''}${userSentryOptions.tunnelRoute}` From e8689a7f838926aea40a43a9967867cdfd5d0f6e Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 20 Nov 2025 15:23:56 +0200 Subject: [PATCH 05/16] feat: drop tunnel requests from middleware execution --- .../utils/dropMiddlewareTunnelRequests.ts | 62 +++++++++++++++++++ packages/nextjs/src/edge/index.ts | 24 +++++++ packages/nextjs/src/server/index.ts | 3 + 3 files changed, 89 insertions(+) create mode 100644 packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts diff --git a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts new file mode 100644 index 000000000000..476298d37f56 --- /dev/null +++ b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts @@ -0,0 +1,62 @@ +import { ATTR_URL_QUERY, SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions'; +import { type Span, type SpanAttributes, GLOBAL_OBJ, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { isSentryRequestSpan } from '@sentry/opentelemetry'; +import { ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes'; +import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached'; + +const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { + _sentryRewritesTunnelPath?: string; +}; + +/** + * Drops spans for tunnel requests from middleware or fetch instrumentation. + * This catches both: + * 1. Requests to the local tunnel route (before rewrite) + * 2. Requests to Sentry ingest (after rewrite) + */ +export function dropMiddlewareTunnelRequests(span: Span, attrs: SpanAttributes | undefined): void { + // Only filter middleware spans or HTTP fetch spans + const isMiddleware = attrs?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute'; + // The fetch span could be originating from rewrites re-writing a tunnel request + // So we want to filter it out + const isFetchSpan = attrs?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.http.otel.node_fetch'; + + // If the span is not a middleware span or a fetch span, return + if (!isMiddleware && !isFetchSpan) { + return; + } + + // Check if this is either a tunnel route request or a Sentry ingest request + const isTunnel = isTunnelRouteSpan(attrs || {}); + const isSentry = isSentryRequestSpan(span); + + if (isTunnel || isSentry) { + // Mark the span to be dropped + span.setAttribute(TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION, true); + } +} + +/** + * Checks if a span's HTTP target matches the tunnel route. + */ +function isTunnelRouteSpan(spanAttributes: Record): boolean { + // Don't use process.env here because it will have a different value in the build and runtime + // We want to use the one in build + const tunnelPath = globalWithInjectedValues._sentryRewritesTunnelPath || process.env._sentryRewritesTunnelPath; + if (!tunnelPath) { + return false; + } + + // Check both http.target (older) and url.query (newer) attributes + // eslint-disable-next-line deprecation/deprecation + const httpTarget = spanAttributes[SEMATTRS_HTTP_TARGET] || spanAttributes[ATTR_URL_QUERY]; + + if (typeof httpTarget === 'string') { + // Extract pathname from the target (e.g., "/tunnel?o=123&p=456" -> "/tunnel") + const pathname = httpTarget.split('?')[0] || ''; + + return pathname.startsWith(tunnelPath); + } + + return false; +} diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 5fd92707b912..091adab98dee 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -1,5 +1,6 @@ import { context } from '@opentelemetry/api'; import { + type EventProcessor, applySdkMetadata, getCapturedScopesOnSpan, getCurrentScope, @@ -19,7 +20,9 @@ import { import { getScopesFromContext } from '@sentry/opentelemetry'; import type { VercelEdgeOptions } from '@sentry/vercel-edge'; import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge'; +import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached'; import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; +import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests'; import { isBuild } from '../common/utils/isBuild'; import { flushSafelyWithTimeout } from '../common/utils/responseEnd'; import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata'; @@ -35,6 +38,7 @@ export type EdgeOptions = VercelEdgeOptions; const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryRewriteFramesDistDir?: string; _sentryRelease?: string; + _sentryRewritesTunnelPath?: string; }; /** Inits the Sentry NextJS SDK on the Edge Runtime. */ @@ -70,6 +74,8 @@ export function init(options: VercelEdgeOptions = {}): void { const rootSpan = getRootSpan(span); const isRootSpan = span === rootSpan; + dropMiddlewareTunnelRequests(span, spanAttributes); + // Mark all spans generated by Next.js as 'auto' if (spanAttributes?.['next.span_type'] !== undefined) { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); @@ -137,6 +143,24 @@ export function init(options: VercelEdgeOptions = {}): void { } }); + getGlobalScope().addEventProcessor( + Object.assign( + (event => { + // Filter transactions that we explicitly want to drop. + if (event.type === 'transaction') { + if (event.contexts?.trace?.data?.[TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION]) { + return null; + } + + return event; + } else { + return event; + } + }) satisfies EventProcessor, + { id: 'NextLowQualityTransactionsFilter' }, + ), + ); + try { // @ts-expect-error `process.turbopack` is a magic string that will be replaced by Next.js if (process.turbopack) { diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index ce8ac7c56cea..caec9a9f1af1 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -38,6 +38,7 @@ import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION, } from '../common/span-attributes-with-logic-attached'; import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; +import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests'; import { isBuild } from '../common/utils/isBuild'; import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; @@ -169,6 +170,8 @@ export function init(options: NodeOptions): NodeClient | undefined { const rootSpan = getRootSpan(span); const isRootSpan = span === rootSpan; + dropMiddlewareTunnelRequests(span, spanAttributes); + // What we do in this glorious piece of code, is hoist any information about parameterized routes from spans emitted // by Next.js via the `next.route` attribute, up to the transaction by setting the http.route attribute. if (typeof spanAttributes?.[ATTR_NEXT_ROUTE] === 'string') { From 44516f57ebca6f41e81084c36f11eeb6f2f0b962 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 21 Nov 2025 11:56:39 +0200 Subject: [PATCH 06/16] feat: added utility for extending the middleware matchers --- packages/nextjs/src/common/index.ts | 1 + .../src/common/withSentryTunnelExclusion.ts | 44 +++++ .../common/withSentryTunnelExclusion.test.ts | 160 ++++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 packages/nextjs/src/common/withSentryTunnelExclusion.ts create mode 100644 packages/nextjs/test/common/withSentryTunnelExclusion.test.ts diff --git a/packages/nextjs/src/common/index.ts b/packages/nextjs/src/common/index.ts index b9a652522349..a762709bf903 100644 --- a/packages/nextjs/src/common/index.ts +++ b/packages/nextjs/src/common/index.ts @@ -12,3 +12,4 @@ export { wrapPageComponentWithSentry } from './pages-router-instrumentation/wrap export { wrapGenerationFunctionWithSentry } from './wrapGenerationFunctionWithSentry'; export { withServerActionInstrumentation } from './withServerActionInstrumentation'; export { captureRequestError } from './captureRequestError'; +export { withSentryTunnelExclusion } from './withSentryTunnelExclusion'; diff --git a/packages/nextjs/src/common/withSentryTunnelExclusion.ts b/packages/nextjs/src/common/withSentryTunnelExclusion.ts new file mode 100644 index 000000000000..afae259d0ad7 --- /dev/null +++ b/packages/nextjs/src/common/withSentryTunnelExclusion.ts @@ -0,0 +1,44 @@ +import { GLOBAL_OBJ } from '@sentry/core'; + +const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { + _sentryRewritesTunnelPath?: string; +}; + +/** + * Wraps a middleware matcher to automatically exclude the Sentry tunnel route. + * + * This is useful when you have a middleware matcher that would otherwise match + * the Sentry tunnel route and potentially interfere with event delivery. + * + * @example + * ```ts + * // middleware.ts + * import { withSentryTunnelExclusion } from '@sentry/nextjs'; + * + * export const config = { + * matcher: withSentryTunnelExclusion([ + * '/api/:path*', + * '/admin/:path*', + * ]), + * }; + * ``` + * + * @param matcher - Your middleware matcher (string or array of strings) + * @returns A matcher that excludes the Sentry tunnel route + */ +export function withSentryTunnelExclusion(matcher: string | string[]): string | string[] { + const tunnelPath = process.env._sentryRewritesTunnelPath || globalWithInjectedValues._sentryRewritesTunnelPath; + if (!tunnelPath) { + return matcher; + } + + // Convert to array for easier handling + const matchers = Array.isArray(matcher) ? matcher : [matcher]; + + // Add negated matcher for the tunnel route + // This tells Next.js to NOT run middleware on the tunnel path + const tunnelExclusion = `/((?!${tunnelPath.replace(/^\//, '')}).*)`; + + // Combine with existing matchers + return [...matchers, tunnelExclusion]; +} diff --git a/packages/nextjs/test/common/withSentryTunnelExclusion.test.ts b/packages/nextjs/test/common/withSentryTunnelExclusion.test.ts new file mode 100644 index 000000000000..0d36721e19a6 --- /dev/null +++ b/packages/nextjs/test/common/withSentryTunnelExclusion.test.ts @@ -0,0 +1,160 @@ +import { GLOBAL_OBJ } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { withSentryTunnelExclusion } from '../../src/common/withSentryTunnelExclusion'; + +const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { + _sentryRewritesTunnelPath?: string | null; +}; + +describe('withSentryTunnelExclusion', () => { + let originalEnv: string | undefined; + let originalGlobal: unknown; + + beforeEach(() => { + // Save original values + originalEnv = process.env._sentryRewritesTunnelPath; + originalGlobal = globalWithInjectedValues._sentryRewritesTunnelPath; + }); + + afterEach(() => { + // Restore original values + if (originalEnv === undefined) { + delete process.env._sentryRewritesTunnelPath; + } else { + process.env._sentryRewritesTunnelPath = originalEnv; + } + + if (originalGlobal === undefined) { + delete globalWithInjectedValues._sentryRewritesTunnelPath; + } else { + // @ts-expect-error - we're resetting the value to the original value + globalWithInjectedValues._sentryRewritesTunnelPath = originalGlobal; + } + }); + + describe('when no tunnel path is configured', () => { + beforeEach(() => { + delete process.env._sentryRewritesTunnelPath; + delete globalWithInjectedValues._sentryRewritesTunnelPath; + }); + + it('should return string matcher unchanged', () => { + const result = withSentryTunnelExclusion('/api/:path*'); + expect(result).toBe('/api/:path*'); + }); + + it('should return array matcher unchanged', () => { + const matcher = ['/api/:path*', '/admin/:path*']; + const result = withSentryTunnelExclusion(matcher); + expect(result).toBe(matcher); + expect(result).toEqual(['/api/:path*', '/admin/:path*']); + }); + }); + + describe('when tunnel path is configured via process.env', () => { + beforeEach(() => { + process.env._sentryRewritesTunnelPath = '/sentry-tunnel'; + }); + + it('should add exclusion pattern to string matcher', () => { + const result = withSentryTunnelExclusion('/api/:path*'); + expect(result).toEqual(['/api/:path*', '/((?!sentry-tunnel).*)']); + }); + + it('should add exclusion pattern to array matcher', () => { + const result = withSentryTunnelExclusion(['/api/:path*', '/admin/:path*']); + expect(result).toEqual(['/api/:path*', '/admin/:path*', '/((?!sentry-tunnel).*)']); + }); + + it('should handle tunnel path without leading slash', () => { + process.env._sentryRewritesTunnelPath = 'tunnel-route'; + const result = withSentryTunnelExclusion('/api/:path*'); + expect(result).toEqual(['/api/:path*', '/((?!tunnel-route).*)']); + }); + + it('should handle tunnel path with leading slash', () => { + process.env._sentryRewritesTunnelPath = '/tunnel-route'; + const result = withSentryTunnelExclusion('/api/:path*'); + expect(result).toEqual(['/api/:path*', '/((?!tunnel-route).*)']); + }); + + it('should work with random generated tunnel paths', () => { + process.env._sentryRewritesTunnelPath = '/abc123xyz'; + const result = withSentryTunnelExclusion(['/api/:path*']); + expect(result).toEqual(['/api/:path*', '/((?!abc123xyz).*)']); + }); + + it('should work with empty array matcher', () => { + const result = withSentryTunnelExclusion([]); + expect(result).toEqual(['/((?!sentry-tunnel).*)']); + }); + }); + + describe('when tunnel path is configured via GLOBAL_OBJ', () => { + beforeEach(() => { + delete process.env._sentryRewritesTunnelPath; + globalWithInjectedValues._sentryRewritesTunnelPath = '/global-tunnel'; + }); + + it('should add exclusion pattern using global value', () => { + const result = withSentryTunnelExclusion('/api/:path*'); + expect(result).toEqual(['/api/:path*', '/((?!global-tunnel).*)']); + }); + + it('should prefer process.env over GLOBAL_OBJ', () => { + process.env._sentryRewritesTunnelPath = '/env-tunnel'; + const result = withSentryTunnelExclusion('/api/:path*'); + expect(result).toEqual(['/api/:path*', '/((?!env-tunnel).*)']); + }); + }); + + describe('edge cases', () => { + beforeEach(() => { + process.env._sentryRewritesTunnelPath = '/tunnel'; + }); + + it('should handle single slash matcher', () => { + const result = withSentryTunnelExclusion('/'); + expect(result).toEqual(['/', '/((?!tunnel).*)']); + }); + + it('should handle complex path patterns', () => { + const result = withSentryTunnelExclusion([ + '/((?!api|_next/static|_next/image|favicon.ico).*)', + '/api/protected/:path*', + ]); + expect(result).toEqual([ + '/((?!api|_next/static|_next/image|favicon.ico).*)', + '/api/protected/:path*', + '/((?!tunnel).*)', + ]); + }); + + it('should handle matcher with special regex characters in tunnel path', () => { + process.env._sentryRewritesTunnelPath = '/tunnel-route-123'; + const result = withSentryTunnelExclusion('/api/:path*'); + expect(result).toEqual(['/api/:path*', '/((?!tunnel-route-123).*)']); + }); + }); + + describe('real-world usage patterns', () => { + beforeEach(() => { + process.env._sentryRewritesTunnelPath = '/monitoring'; + }); + + it('should work with typical API route matchers', () => { + const result = withSentryTunnelExclusion(['/api/:path*', '/trpc/:path*']); + expect(result).toEqual(['/api/:path*', '/trpc/:path*', '/((?!monitoring).*)']); + }); + + it('should work with exclusion-based matchers', () => { + const result = withSentryTunnelExclusion('/((?!_next/static|_next/image|favicon.ico).*)'); + expect(result).toEqual(['/((?!_next/static|_next/image|favicon.ico).*)', '/((?!monitoring).*)']); + }); + + it('should work with admin and protected routes', () => { + const result = withSentryTunnelExclusion(['/admin/:path*', '/dashboard/:path*', '/api/auth/:path*']); + expect(result).toEqual(['/admin/:path*', '/dashboard/:path*', '/api/auth/:path*', '/((?!monitoring).*)']); + }); + }); +}); From 731607874c3c419f228df2dcfffb2186de293717 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 21 Nov 2025 12:02:10 +0200 Subject: [PATCH 07/16] feat: added warning if the user hasn't configured the matcher properly --- packages/nextjs/src/config/util.ts | 34 +++++ .../nextjs/src/config/withSentryConfig.ts | 47 ++++++ packages/nextjs/test/config/util.test.ts | 134 +++++++++++++++++- 3 files changed, 214 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/util.ts b/packages/nextjs/src/config/util.ts index 0d4a55687d2f..277ba3660713 100644 --- a/packages/nextjs/src/config/util.ts +++ b/packages/nextjs/src/config/util.ts @@ -181,3 +181,37 @@ export function detectActiveBundler(): 'turbopack' | 'webpack' { return 'webpack'; } } + +/** + * Finds the middleware or proxy file in the Next.js project. + * Next.js only allows one middleware file, so this returns the first match. + */ +export function findMiddlewareFile(): { path: string; contents: string } | undefined { + const projectDir = process.cwd(); + + // In Next.js 16+, the file is called 'proxy', in earlier versions it's 'middleware' + const nextVersion = getNextjsVersion(); + const nextMajor = nextVersion ? parseSemver(nextVersion).major : undefined; + const basename = nextMajor && nextMajor >= 16 ? 'proxy' : 'middleware'; + const directories = [projectDir, `${projectDir}/src`]; + const extensions = ['.ts', '.js']; + + // Find the first existing middleware/proxy file + for (const dir of directories) { + for (const ext of extensions) { + const filePath = `${dir}/${basename}${ext}`; + if (fs.existsSync(filePath)) { + try { + const contents = fs.readFileSync(filePath, 'utf-8'); + + return { path: filePath, contents }; + } catch { + // If we can't read the file, continue searching + continue; + } + } + } + } + + return undefined; +} diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 43455f06f54d..6248cdc5771b 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -18,6 +18,7 @@ import type { } from './types'; import { detectActiveBundler, + findMiddlewareFile, getNextjsVersion, requiresInstrumentationHook, supportsProductionCompileHook, @@ -26,6 +27,7 @@ import { constructWebpackConfigFunction } from './webpack'; let showedExportModeTunnelWarning = false; let showedExperimentalBuildModeWarning = false; +let showedMiddlewareMatcherWarning = false; // Packages we auto-instrument need to be external for instrumentation to work // Next.js externalizes some packages by default, see: https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages @@ -89,6 +91,50 @@ export function withSentryConfig(nextConfig?: C, sentryBuildOptions: SentryBu } } +/** + * Checks if the user has a middleware/proxy file with a matcher that might exclude the tunnel route. + * Warns the user if they have a matcher but are not using withSentryTunnelExclusion. + */ +function checkMiddlewareMatcherForTunnelRoute(tunnelPath: string): void { + if (showedMiddlewareMatcherWarning) { + return; + } + + try { + const middlewareFile = findMiddlewareFile(); + + // No middleware file found + if (!middlewareFile) { + return; + } + + // Check if they're already using withSentryTunnelExclusion + if (middlewareFile.contents.includes('withSentryTunnelExclusion')) { + return; + } + + // Look for config.matcher export + const isProxy = middlewareFile.path.includes('proxy'); + const hasConfigMatcher = /export\s+const\s+config\s*=\s*{[^}]*matcher\s*:/s.test(middlewareFile.contents); + + if (hasConfigMatcher) { + // eslint-disable-next-line no-console + console.warn( + `[@sentry/nextjs] WARNING: You have a ${isProxy ? 'proxy' : 'middleware'} file (${path.basename(middlewareFile.path)}) with a \`config.matcher\`. ` + + `If your matcher does not include the Sentry tunnel route (${tunnelPath}), tunnel requests may be blocked. ` + + "To ensure your matcher doesn't interfere with Sentry event delivery, wrap your matcher with `withSentryTunnelExclusion`:\n\n" + + " import { withSentryTunnelExclusion } from '@sentry/nextjs';\n" + + ' export const config = {\n' + + " matcher: withSentryTunnelExclusion(['/your/routes']),\n" + + ' };\n', + ); + showedMiddlewareMatcherWarning = true; + } + } catch { + // Silently fail - this is just a helpful warning, not critical + } +} + /** * Generates a random tunnel route path that's less likely to be blocked by ad-blockers */ @@ -126,6 +172,7 @@ function getFinalConfigObject( userSentryOptions.tunnelRoute = resolvedTunnelRoute || undefined; setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute); + checkMiddlewareMatcherForTunnelRoute(resolvedTunnelRoute); } } diff --git a/packages/nextjs/test/config/util.test.ts b/packages/nextjs/test/config/util.test.ts index 7335139b5037..6f1f849268c3 100644 --- a/packages/nextjs/test/config/util.test.ts +++ b/packages/nextjs/test/config/util.test.ts @@ -1,4 +1,6 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import * as util from '../../src/config/util'; describe('util', () => { @@ -334,4 +336,134 @@ describe('util', () => { expect(util.detectActiveBundler()).toBe('webpack'); }); }); + + describe('findMiddlewareFile', () => { + vi.mock('../../src/config/util', async () => { + const actual = await vi.importActual('../../src/config/util'); + return { + ...actual, + getNextjsVersion: vi.fn(), + }; + }); + + let originalCwd: string; + let testDir: string; + + beforeEach(() => { + originalCwd = process.cwd(); + testDir = path.join(__dirname, '.test-middleware-temp'); + + // Create test directory + if (!fs.existsSync(testDir)) { + fs.mkdirSync(testDir, { recursive: true }); + } + + process.chdir(testDir); + }); + + afterEach(() => { + process.chdir(originalCwd); + + // Clean up test directory + if (fs.existsSync(testDir)) { + fs.rmSync(testDir, { recursive: true, force: true }); + } + + vi.clearAllMocks(); + }); + + describe('Next.js <16 (middleware)', () => { + beforeEach(() => { + vi.mocked(util.getNextjsVersion).mockReturnValue('15.0.0'); + }); + + it('should find middleware.ts in root directory', () => { + fs.writeFileSync(path.join(testDir, 'middleware.ts'), 'export default function middleware() {}'); + + const result = util.findMiddlewareFile(); + + expect(result).toBeDefined(); + expect(result?.path).toContain('middleware.ts'); + expect(result?.contents).toBe('export default function middleware() {}'); + }); + + it('should find middleware.js in root directory', () => { + fs.writeFileSync(path.join(testDir, 'middleware.js'), 'module.exports = function middleware() {}'); + + const result = util.findMiddlewareFile(); + + expect(result).toBeDefined(); + expect(result?.path).toContain('middleware.js'); + expect(result?.contents).toBe('module.exports = function middleware() {}'); + }); + + it('should find middleware.ts in src directory', () => { + fs.mkdirSync(path.join(testDir, 'src'), { recursive: true }); + fs.writeFileSync(path.join(testDir, 'src', 'middleware.ts'), 'export default function middleware() {}'); + + const result = util.findMiddlewareFile(); + + expect(result).toBeDefined(); + expect(result?.path).toContain(path.join('src', 'middleware.ts')); + expect(result?.contents).toBe('export default function middleware() {}'); + }); + + it('should prefer root over src directory', () => { + fs.mkdirSync(path.join(testDir, 'src'), { recursive: true }); + fs.writeFileSync(path.join(testDir, 'middleware.ts'), 'root middleware'); + fs.writeFileSync(path.join(testDir, 'src', 'middleware.ts'), 'src middleware'); + + const result = util.findMiddlewareFile(); + + expect(result).toBeDefined(); + expect(result?.contents).toBe('root middleware'); + }); + + it('should prefer .ts over .js extension', () => { + fs.writeFileSync(path.join(testDir, 'middleware.ts'), 'typescript middleware'); + fs.writeFileSync(path.join(testDir, 'middleware.js'), 'javascript middleware'); + + const result = util.findMiddlewareFile(); + + expect(result).toBeDefined(); + expect(result?.contents).toBe('typescript middleware'); + }); + + it('should NOT find proxy.ts on Next.js <16', () => { + fs.writeFileSync(path.join(testDir, 'proxy.ts'), 'export default function proxy() {}'); + + const result = util.findMiddlewareFile(); + + expect(result).toBeUndefined(); + }); + + it('should return undefined when no middleware file exists', () => { + const result = util.findMiddlewareFile(); + + expect(result).toBeUndefined(); + }); + }); + + // Note: Tests for Next.js 16+ (proxy) behavior would require mocking getNextjsVersion, + // which is challenging in this test setup due to module imports. + // The logic is the same as middleware tests but with 'proxy' instead of 'middleware'. + + describe('edge cases', () => { + beforeEach(() => { + vi.mocked(util.getNextjsVersion).mockReturnValue('15.0.0'); + }); + + it('should handle when getNextjsVersion returns undefined', () => { + vi.mocked(util.getNextjsVersion).mockReturnValue(undefined); + + fs.writeFileSync(path.join(testDir, 'middleware.ts'), 'export default function middleware() {}'); + + const result = util.findMiddlewareFile(); + + // Should default to 'middleware' when version is unknown + expect(result).toBeDefined(); + expect(result?.path).toContain('middleware.ts'); + }); + }); + }); }); From 1a231a440a9714e7026a8dd9f1ba4d6f9722a08e Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 21 Nov 2025 12:09:37 +0200 Subject: [PATCH 08/16] ref: better DX with config wrapping instead of property wrapping --- packages/nextjs/src/common/index.ts | 2 +- .../src/common/withSentryMiddlewareConfig.ts | 78 ++++++ .../src/common/withSentryTunnelExclusion.ts | 44 ---- .../nextjs/src/config/withSentryConfig.ts | 20 +- .../common/withSentryMiddlewareConfig.test.ts | 230 ++++++++++++++++++ .../common/withSentryTunnelExclusion.test.ts | 160 ------------ 6 files changed, 321 insertions(+), 213 deletions(-) create mode 100644 packages/nextjs/src/common/withSentryMiddlewareConfig.ts delete mode 100644 packages/nextjs/src/common/withSentryTunnelExclusion.ts create mode 100644 packages/nextjs/test/common/withSentryMiddlewareConfig.test.ts delete mode 100644 packages/nextjs/test/common/withSentryTunnelExclusion.test.ts diff --git a/packages/nextjs/src/common/index.ts b/packages/nextjs/src/common/index.ts index a762709bf903..869398417149 100644 --- a/packages/nextjs/src/common/index.ts +++ b/packages/nextjs/src/common/index.ts @@ -12,4 +12,4 @@ export { wrapPageComponentWithSentry } from './pages-router-instrumentation/wrap export { wrapGenerationFunctionWithSentry } from './wrapGenerationFunctionWithSentry'; export { withServerActionInstrumentation } from './withServerActionInstrumentation'; export { captureRequestError } from './captureRequestError'; -export { withSentryTunnelExclusion } from './withSentryTunnelExclusion'; +export { withSentryMiddlewareConfig, withSentryProxyConfig } from './withSentryMiddlewareConfig'; diff --git a/packages/nextjs/src/common/withSentryMiddlewareConfig.ts b/packages/nextjs/src/common/withSentryMiddlewareConfig.ts new file mode 100644 index 000000000000..2b67d2479f63 --- /dev/null +++ b/packages/nextjs/src/common/withSentryMiddlewareConfig.ts @@ -0,0 +1,78 @@ +import { GLOBAL_OBJ } from '@sentry/core'; + +const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { + _sentryRewritesTunnelPath?: string; +}; + +/** + * Middleware config type for Next.js + */ +type MiddlewareConfig = { + [key: string]: unknown; + matcher?: string | string[]; +}; + +/** + * Configures middleware/proxy settings with Sentry-specific adjustments. + * Automatically excludes the Sentry tunnel route from the matcher to prevent interference. + * + * @example + * ```ts + * // middleware.ts (Next.js <16) + * import { withSentryMiddlewareConfig } from '@sentry/nextjs'; + * + * export const config = withSentryMiddlewareConfig({ + * matcher: ['/api/:path*', '/admin/:path*'], + * }); + * ``` + * + * @example + * ```ts + * // proxy.ts (Next.js 16+) + * import { withSentryProxyConfig } from '@sentry/nextjs'; + * + * export const config = withSentryProxyConfig({ + * matcher: ['/api/:path*', '/admin/:path*'], + * }); + * ``` + * + * @param config - Middleware/proxy configuration object + * @returns Updated config with Sentry tunnel route excluded from matcher + */ +export function withSentryMiddlewareConfig(config: MiddlewareConfig): MiddlewareConfig { + const tunnelPath = process.env._sentryRewritesTunnelPath || globalWithInjectedValues._sentryRewritesTunnelPath; + + // If no tunnel path or no matcher, return config as-is + if (!tunnelPath || !config.matcher) { + return config; + } + + // Convert to array for easier handling + const matchers = Array.isArray(config.matcher) ? config.matcher : [config.matcher]; + + // Add negated matcher for the tunnel route + // This tells Next.js to NOT run middleware on the tunnel path + const tunnelExclusion = `/((?!${tunnelPath.replace(/^\//, '')}).*)`; + + // Return updated config with tunnel exclusion + return { + ...config, + matcher: [...matchers, tunnelExclusion], + }; +} + +/** + * Alias for `withSentryMiddlewareConfig` to support Next.js 16+ terminology. + * In Next.js 16+, middleware files are called "proxy" files. + * + * @example + * ```ts + * // proxy.ts (Next.js 16+) + * import { withSentryProxyConfig } from '@sentry/nextjs'; + * + * export const config = withSentryProxyConfig({ + * matcher: ['/api/:path*', '/admin/:path*'], + * }); + * ``` + */ +export const withSentryProxyConfig = withSentryMiddlewareConfig; diff --git a/packages/nextjs/src/common/withSentryTunnelExclusion.ts b/packages/nextjs/src/common/withSentryTunnelExclusion.ts deleted file mode 100644 index afae259d0ad7..000000000000 --- a/packages/nextjs/src/common/withSentryTunnelExclusion.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { GLOBAL_OBJ } from '@sentry/core'; - -const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { - _sentryRewritesTunnelPath?: string; -}; - -/** - * Wraps a middleware matcher to automatically exclude the Sentry tunnel route. - * - * This is useful when you have a middleware matcher that would otherwise match - * the Sentry tunnel route and potentially interfere with event delivery. - * - * @example - * ```ts - * // middleware.ts - * import { withSentryTunnelExclusion } from '@sentry/nextjs'; - * - * export const config = { - * matcher: withSentryTunnelExclusion([ - * '/api/:path*', - * '/admin/:path*', - * ]), - * }; - * ``` - * - * @param matcher - Your middleware matcher (string or array of strings) - * @returns A matcher that excludes the Sentry tunnel route - */ -export function withSentryTunnelExclusion(matcher: string | string[]): string | string[] { - const tunnelPath = process.env._sentryRewritesTunnelPath || globalWithInjectedValues._sentryRewritesTunnelPath; - if (!tunnelPath) { - return matcher; - } - - // Convert to array for easier handling - const matchers = Array.isArray(matcher) ? matcher : [matcher]; - - // Add negated matcher for the tunnel route - // This tells Next.js to NOT run middleware on the tunnel path - const tunnelExclusion = `/((?!${tunnelPath.replace(/^\//, '')}).*)`; - - // Combine with existing matchers - return [...matchers, tunnelExclusion]; -} diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 6248cdc5771b..e96dd5d93be2 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -93,7 +93,7 @@ export function withSentryConfig(nextConfig?: C, sentryBuildOptions: SentryBu /** * Checks if the user has a middleware/proxy file with a matcher that might exclude the tunnel route. - * Warns the user if they have a matcher but are not using withSentryTunnelExclusion. + * Warns the user if they have a matcher but are not using withSentryMiddlewareConfig or withSentryProxyConfig. */ function checkMiddlewareMatcherForTunnelRoute(tunnelPath: string): void { if (showedMiddlewareMatcherWarning) { @@ -108,8 +108,11 @@ function checkMiddlewareMatcherForTunnelRoute(tunnelPath: string): void { return; } - // Check if they're already using withSentryTunnelExclusion - if (middlewareFile.contents.includes('withSentryTunnelExclusion')) { + // Check if they're already using Sentry middleware/proxy config helpers + if ( + middlewareFile.contents.includes('withSentryMiddlewareConfig') || + middlewareFile.contents.includes('withSentryProxyConfig') + ) { return; } @@ -118,15 +121,16 @@ function checkMiddlewareMatcherForTunnelRoute(tunnelPath: string): void { const hasConfigMatcher = /export\s+const\s+config\s*=\s*{[^}]*matcher\s*:/s.test(middlewareFile.contents); if (hasConfigMatcher) { + const helperName = isProxy ? 'withSentryProxyConfig' : 'withSentryMiddlewareConfig'; // eslint-disable-next-line no-console console.warn( `[@sentry/nextjs] WARNING: You have a ${isProxy ? 'proxy' : 'middleware'} file (${path.basename(middlewareFile.path)}) with a \`config.matcher\`. ` + `If your matcher does not include the Sentry tunnel route (${tunnelPath}), tunnel requests may be blocked. ` + - "To ensure your matcher doesn't interfere with Sentry event delivery, wrap your matcher with `withSentryTunnelExclusion`:\n\n" + - " import { withSentryTunnelExclusion } from '@sentry/nextjs';\n" + - ' export const config = {\n' + - " matcher: withSentryTunnelExclusion(['/your/routes']),\n" + - ' };\n', + `To ensure your matcher doesn't interfere with Sentry event delivery, wrap your config with \`${helperName}\`:\n\n` + + ` import { ${helperName} } from '@sentry/nextjs';\n` + + ` export const config = ${helperName}({\n` + + " matcher: ['/your/routes'],\n" + + ' });\n', ); showedMiddlewareMatcherWarning = true; } diff --git a/packages/nextjs/test/common/withSentryMiddlewareConfig.test.ts b/packages/nextjs/test/common/withSentryMiddlewareConfig.test.ts new file mode 100644 index 000000000000..8399452b7723 --- /dev/null +++ b/packages/nextjs/test/common/withSentryMiddlewareConfig.test.ts @@ -0,0 +1,230 @@ +import { GLOBAL_OBJ } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { withSentryMiddlewareConfig, withSentryProxyConfig } from '../../src/common/withSentryMiddlewareConfig'; + +const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { + _sentryRewritesTunnelPath?: string | null; +}; + +describe('withSentryMiddlewareConfig', () => { + let originalEnv: string | undefined; + let originalGlobal: unknown; + + beforeEach(() => { + // Save original values + originalEnv = process.env._sentryRewritesTunnelPath; + originalGlobal = globalWithInjectedValues._sentryRewritesTunnelPath; + }); + + afterEach(() => { + // Restore original values + if (originalEnv === undefined) { + delete process.env._sentryRewritesTunnelPath; + } else { + process.env._sentryRewritesTunnelPath = originalEnv; + } + + if (originalGlobal === undefined) { + delete globalWithInjectedValues._sentryRewritesTunnelPath; + } else { + // @ts-expect-error - we're resetting the value to the original value + globalWithInjectedValues._sentryRewritesTunnelPath = originalGlobal; + } + }); + + describe('when no tunnel path is configured', () => { + beforeEach(() => { + delete process.env._sentryRewritesTunnelPath; + delete globalWithInjectedValues._sentryRewritesTunnelPath; + }); + + it('should return config unchanged', () => { + const config = { matcher: ['/api/:path*', '/admin/:path*'] }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual(config); + }); + + it('should return config with no matcher unchanged', () => { + const config = { someOtherOption: true }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual(config); + }); + + it('should preserve other config properties', () => { + const config = { matcher: ['/api/:path*'], regions: ['us-east-1'], custom: true }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual(config); + }); + }); + + describe('when tunnel path is configured via process.env', () => { + beforeEach(() => { + process.env._sentryRewritesTunnelPath = '/sentry-tunnel'; + }); + + it('should add exclusion pattern to config with string matcher', () => { + const config = { matcher: '/api/:path*' }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/api/:path*', '/((?!sentry-tunnel).*)'], + }); + }); + + it('should add exclusion pattern to config with array matcher', () => { + const config = { matcher: ['/api/:path*', '/admin/:path*'] }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/api/:path*', '/admin/:path*', '/((?!sentry-tunnel).*)'], + }); + }); + + it('should handle tunnel path without leading slash', () => { + process.env._sentryRewritesTunnelPath = 'tunnel-route'; + const config = { matcher: '/api/:path*' }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/api/:path*', '/((?!tunnel-route).*)'], + }); + }); + + it('should handle tunnel path with leading slash', () => { + process.env._sentryRewritesTunnelPath = '/tunnel-route'; + const config = { matcher: '/api/:path*' }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/api/:path*', '/((?!tunnel-route).*)'], + }); + }); + + it('should work with random generated tunnel paths', () => { + process.env._sentryRewritesTunnelPath = '/abc123xyz'; + const config = { matcher: ['/api/:path*'] }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/api/:path*', '/((?!abc123xyz).*)'], + }); + }); + + it('should work with empty array matcher', () => { + const config = { matcher: [] }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/((?!sentry-tunnel).*)'], + }); + }); + + it('should preserve other config properties', () => { + const config = { matcher: ['/api/:path*'], regions: ['us-east-1'], custom: true }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/api/:path*', '/((?!sentry-tunnel).*)'], + regions: ['us-east-1'], + custom: true, + }); + }); + }); + + describe('when tunnel path is configured via GLOBAL_OBJ', () => { + beforeEach(() => { + delete process.env._sentryRewritesTunnelPath; + globalWithInjectedValues._sentryRewritesTunnelPath = '/global-tunnel'; + }); + + it('should add exclusion pattern using global value', () => { + const config = { matcher: '/api/:path*' }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/api/:path*', '/((?!global-tunnel).*)'], + }); + }); + + it('should prefer process.env over GLOBAL_OBJ', () => { + process.env._sentryRewritesTunnelPath = '/env-tunnel'; + const config = { matcher: '/api/:path*' }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/api/:path*', '/((?!env-tunnel).*)'], + }); + }); + }); + + describe('edge cases', () => { + beforeEach(() => { + process.env._sentryRewritesTunnelPath = '/tunnel'; + }); + + it('should handle single slash matcher', () => { + const config = { matcher: '/' }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/', '/((?!tunnel).*)'], + }); + }); + + it('should handle complex path patterns', () => { + const config = { + matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)', '/api/protected/:path*'], + }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)', '/api/protected/:path*', '/((?!tunnel).*)'], + }); + }); + + it('should handle matcher with special regex characters in tunnel path', () => { + process.env._sentryRewritesTunnelPath = '/tunnel-route-123'; + const config = { matcher: '/api/:path*' }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/api/:path*', '/((?!tunnel-route-123).*)'], + }); + }); + }); + + describe('real-world usage patterns', () => { + beforeEach(() => { + process.env._sentryRewritesTunnelPath = '/monitoring'; + }); + + it('should work with typical API route matchers', () => { + const config = { matcher: ['/api/:path*', '/trpc/:path*'] }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/api/:path*', '/trpc/:path*', '/((?!monitoring).*)'], + }); + }); + + it('should work with exclusion-based matchers', () => { + const config = { matcher: '/((?!_next/static|_next/image|favicon.ico).*)' }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/((?!_next/static|_next/image|favicon.ico).*)', '/((?!monitoring).*)'], + }); + }); + + it('should work with admin and protected routes', () => { + const config = { matcher: ['/admin/:path*', '/dashboard/:path*', '/api/auth/:path*'] }; + const result = withSentryMiddlewareConfig(config); + expect(result).toEqual({ + matcher: ['/admin/:path*', '/dashboard/:path*', '/api/auth/:path*', '/((?!monitoring).*)'], + }); + }); + }); + + describe('withSentryProxyConfig alias', () => { + beforeEach(() => { + process.env._sentryRewritesTunnelPath = '/sentry-tunnel'; + }); + + it('should be an alias for withSentryMiddlewareConfig', () => { + expect(withSentryProxyConfig).toBe(withSentryMiddlewareConfig); + }); + + it('should work identically to withSentryMiddlewareConfig', () => { + const config = { matcher: ['/api/:path*'] }; + const resultMiddleware = withSentryMiddlewareConfig(config); + const resultProxy = withSentryProxyConfig(config); + expect(resultProxy).toEqual(resultMiddleware); + }); + }); +}); diff --git a/packages/nextjs/test/common/withSentryTunnelExclusion.test.ts b/packages/nextjs/test/common/withSentryTunnelExclusion.test.ts deleted file mode 100644 index 0d36721e19a6..000000000000 --- a/packages/nextjs/test/common/withSentryTunnelExclusion.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { GLOBAL_OBJ } from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { withSentryTunnelExclusion } from '../../src/common/withSentryTunnelExclusion'; - -const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { - _sentryRewritesTunnelPath?: string | null; -}; - -describe('withSentryTunnelExclusion', () => { - let originalEnv: string | undefined; - let originalGlobal: unknown; - - beforeEach(() => { - // Save original values - originalEnv = process.env._sentryRewritesTunnelPath; - originalGlobal = globalWithInjectedValues._sentryRewritesTunnelPath; - }); - - afterEach(() => { - // Restore original values - if (originalEnv === undefined) { - delete process.env._sentryRewritesTunnelPath; - } else { - process.env._sentryRewritesTunnelPath = originalEnv; - } - - if (originalGlobal === undefined) { - delete globalWithInjectedValues._sentryRewritesTunnelPath; - } else { - // @ts-expect-error - we're resetting the value to the original value - globalWithInjectedValues._sentryRewritesTunnelPath = originalGlobal; - } - }); - - describe('when no tunnel path is configured', () => { - beforeEach(() => { - delete process.env._sentryRewritesTunnelPath; - delete globalWithInjectedValues._sentryRewritesTunnelPath; - }); - - it('should return string matcher unchanged', () => { - const result = withSentryTunnelExclusion('/api/:path*'); - expect(result).toBe('/api/:path*'); - }); - - it('should return array matcher unchanged', () => { - const matcher = ['/api/:path*', '/admin/:path*']; - const result = withSentryTunnelExclusion(matcher); - expect(result).toBe(matcher); - expect(result).toEqual(['/api/:path*', '/admin/:path*']); - }); - }); - - describe('when tunnel path is configured via process.env', () => { - beforeEach(() => { - process.env._sentryRewritesTunnelPath = '/sentry-tunnel'; - }); - - it('should add exclusion pattern to string matcher', () => { - const result = withSentryTunnelExclusion('/api/:path*'); - expect(result).toEqual(['/api/:path*', '/((?!sentry-tunnel).*)']); - }); - - it('should add exclusion pattern to array matcher', () => { - const result = withSentryTunnelExclusion(['/api/:path*', '/admin/:path*']); - expect(result).toEqual(['/api/:path*', '/admin/:path*', '/((?!sentry-tunnel).*)']); - }); - - it('should handle tunnel path without leading slash', () => { - process.env._sentryRewritesTunnelPath = 'tunnel-route'; - const result = withSentryTunnelExclusion('/api/:path*'); - expect(result).toEqual(['/api/:path*', '/((?!tunnel-route).*)']); - }); - - it('should handle tunnel path with leading slash', () => { - process.env._sentryRewritesTunnelPath = '/tunnel-route'; - const result = withSentryTunnelExclusion('/api/:path*'); - expect(result).toEqual(['/api/:path*', '/((?!tunnel-route).*)']); - }); - - it('should work with random generated tunnel paths', () => { - process.env._sentryRewritesTunnelPath = '/abc123xyz'; - const result = withSentryTunnelExclusion(['/api/:path*']); - expect(result).toEqual(['/api/:path*', '/((?!abc123xyz).*)']); - }); - - it('should work with empty array matcher', () => { - const result = withSentryTunnelExclusion([]); - expect(result).toEqual(['/((?!sentry-tunnel).*)']); - }); - }); - - describe('when tunnel path is configured via GLOBAL_OBJ', () => { - beforeEach(() => { - delete process.env._sentryRewritesTunnelPath; - globalWithInjectedValues._sentryRewritesTunnelPath = '/global-tunnel'; - }); - - it('should add exclusion pattern using global value', () => { - const result = withSentryTunnelExclusion('/api/:path*'); - expect(result).toEqual(['/api/:path*', '/((?!global-tunnel).*)']); - }); - - it('should prefer process.env over GLOBAL_OBJ', () => { - process.env._sentryRewritesTunnelPath = '/env-tunnel'; - const result = withSentryTunnelExclusion('/api/:path*'); - expect(result).toEqual(['/api/:path*', '/((?!env-tunnel).*)']); - }); - }); - - describe('edge cases', () => { - beforeEach(() => { - process.env._sentryRewritesTunnelPath = '/tunnel'; - }); - - it('should handle single slash matcher', () => { - const result = withSentryTunnelExclusion('/'); - expect(result).toEqual(['/', '/((?!tunnel).*)']); - }); - - it('should handle complex path patterns', () => { - const result = withSentryTunnelExclusion([ - '/((?!api|_next/static|_next/image|favicon.ico).*)', - '/api/protected/:path*', - ]); - expect(result).toEqual([ - '/((?!api|_next/static|_next/image|favicon.ico).*)', - '/api/protected/:path*', - '/((?!tunnel).*)', - ]); - }); - - it('should handle matcher with special regex characters in tunnel path', () => { - process.env._sentryRewritesTunnelPath = '/tunnel-route-123'; - const result = withSentryTunnelExclusion('/api/:path*'); - expect(result).toEqual(['/api/:path*', '/((?!tunnel-route-123).*)']); - }); - }); - - describe('real-world usage patterns', () => { - beforeEach(() => { - process.env._sentryRewritesTunnelPath = '/monitoring'; - }); - - it('should work with typical API route matchers', () => { - const result = withSentryTunnelExclusion(['/api/:path*', '/trpc/:path*']); - expect(result).toEqual(['/api/:path*', '/trpc/:path*', '/((?!monitoring).*)']); - }); - - it('should work with exclusion-based matchers', () => { - const result = withSentryTunnelExclusion('/((?!_next/static|_next/image|favicon.ico).*)'); - expect(result).toEqual(['/((?!_next/static|_next/image|favicon.ico).*)', '/((?!monitoring).*)']); - }); - - it('should work with admin and protected routes', () => { - const result = withSentryTunnelExclusion(['/admin/:path*', '/dashboard/:path*', '/api/auth/:path*']); - expect(result).toEqual(['/admin/:path*', '/dashboard/:path*', '/api/auth/:path*', '/((?!monitoring).*)']); - }); - }); -}); From 3890823465fe86ae5e75a7cba1d55aa2680c89b5 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 21 Nov 2025 12:34:53 +0200 Subject: [PATCH 09/16] ref: remove the middleware, config must be statically analyzable --- packages/nextjs/src/common/index.ts | 1 - .../src/common/withSentryMiddlewareConfig.ts | 78 ------ .../nextjs/src/config/withSentryConfig.ts | 23 +- .../common/withSentryMiddlewareConfig.test.ts | 230 ------------------ 4 files changed, 7 insertions(+), 325 deletions(-) delete mode 100644 packages/nextjs/src/common/withSentryMiddlewareConfig.ts delete mode 100644 packages/nextjs/test/common/withSentryMiddlewareConfig.test.ts diff --git a/packages/nextjs/src/common/index.ts b/packages/nextjs/src/common/index.ts index 869398417149..b9a652522349 100644 --- a/packages/nextjs/src/common/index.ts +++ b/packages/nextjs/src/common/index.ts @@ -12,4 +12,3 @@ export { wrapPageComponentWithSentry } from './pages-router-instrumentation/wrap export { wrapGenerationFunctionWithSentry } from './wrapGenerationFunctionWithSentry'; export { withServerActionInstrumentation } from './withServerActionInstrumentation'; export { captureRequestError } from './captureRequestError'; -export { withSentryMiddlewareConfig, withSentryProxyConfig } from './withSentryMiddlewareConfig'; diff --git a/packages/nextjs/src/common/withSentryMiddlewareConfig.ts b/packages/nextjs/src/common/withSentryMiddlewareConfig.ts deleted file mode 100644 index 2b67d2479f63..000000000000 --- a/packages/nextjs/src/common/withSentryMiddlewareConfig.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { GLOBAL_OBJ } from '@sentry/core'; - -const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { - _sentryRewritesTunnelPath?: string; -}; - -/** - * Middleware config type for Next.js - */ -type MiddlewareConfig = { - [key: string]: unknown; - matcher?: string | string[]; -}; - -/** - * Configures middleware/proxy settings with Sentry-specific adjustments. - * Automatically excludes the Sentry tunnel route from the matcher to prevent interference. - * - * @example - * ```ts - * // middleware.ts (Next.js <16) - * import { withSentryMiddlewareConfig } from '@sentry/nextjs'; - * - * export const config = withSentryMiddlewareConfig({ - * matcher: ['/api/:path*', '/admin/:path*'], - * }); - * ``` - * - * @example - * ```ts - * // proxy.ts (Next.js 16+) - * import { withSentryProxyConfig } from '@sentry/nextjs'; - * - * export const config = withSentryProxyConfig({ - * matcher: ['/api/:path*', '/admin/:path*'], - * }); - * ``` - * - * @param config - Middleware/proxy configuration object - * @returns Updated config with Sentry tunnel route excluded from matcher - */ -export function withSentryMiddlewareConfig(config: MiddlewareConfig): MiddlewareConfig { - const tunnelPath = process.env._sentryRewritesTunnelPath || globalWithInjectedValues._sentryRewritesTunnelPath; - - // If no tunnel path or no matcher, return config as-is - if (!tunnelPath || !config.matcher) { - return config; - } - - // Convert to array for easier handling - const matchers = Array.isArray(config.matcher) ? config.matcher : [config.matcher]; - - // Add negated matcher for the tunnel route - // This tells Next.js to NOT run middleware on the tunnel path - const tunnelExclusion = `/((?!${tunnelPath.replace(/^\//, '')}).*)`; - - // Return updated config with tunnel exclusion - return { - ...config, - matcher: [...matchers, tunnelExclusion], - }; -} - -/** - * Alias for `withSentryMiddlewareConfig` to support Next.js 16+ terminology. - * In Next.js 16+, middleware files are called "proxy" files. - * - * @example - * ```ts - * // proxy.ts (Next.js 16+) - * import { withSentryProxyConfig } from '@sentry/nextjs'; - * - * export const config = withSentryProxyConfig({ - * matcher: ['/api/:path*', '/admin/:path*'], - * }); - * ``` - */ -export const withSentryProxyConfig = withSentryMiddlewareConfig; diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index e96dd5d93be2..18e1d2e90352 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -93,7 +93,7 @@ export function withSentryConfig(nextConfig?: C, sentryBuildOptions: SentryBu /** * Checks if the user has a middleware/proxy file with a matcher that might exclude the tunnel route. - * Warns the user if they have a matcher but are not using withSentryMiddlewareConfig or withSentryProxyConfig. + * Warns the user if their matcher might interfere with the tunnel route. */ function checkMiddlewareMatcherForTunnelRoute(tunnelPath: string): void { if (showedMiddlewareMatcherWarning) { @@ -108,29 +108,20 @@ function checkMiddlewareMatcherForTunnelRoute(tunnelPath: string): void { return; } - // Check if they're already using Sentry middleware/proxy config helpers - if ( - middlewareFile.contents.includes('withSentryMiddlewareConfig') || - middlewareFile.contents.includes('withSentryProxyConfig') - ) { - return; - } - // Look for config.matcher export const isProxy = middlewareFile.path.includes('proxy'); const hasConfigMatcher = /export\s+const\s+config\s*=\s*{[^}]*matcher\s*:/s.test(middlewareFile.contents); if (hasConfigMatcher) { - const helperName = isProxy ? 'withSentryProxyConfig' : 'withSentryMiddlewareConfig'; // eslint-disable-next-line no-console console.warn( `[@sentry/nextjs] WARNING: You have a ${isProxy ? 'proxy' : 'middleware'} file (${path.basename(middlewareFile.path)}) with a \`config.matcher\`. ` + - `If your matcher does not include the Sentry tunnel route (${tunnelPath}), tunnel requests may be blocked. ` + - `To ensure your matcher doesn't interfere with Sentry event delivery, wrap your config with \`${helperName}\`:\n\n` + - ` import { ${helperName} } from '@sentry/nextjs';\n` + - ` export const config = ${helperName}({\n` + - " matcher: ['/your/routes'],\n" + - ' });\n', + `If your matcher runs on the Sentry tunnel route (${tunnelPath}), it may interfere with event delivery. ` + + 'Please ensure your matcher excludes the tunnel route. For example:\n\n' + + ' export const config = {\n' + + ' // Use a negative lookahead to exclude the Sentry tunnel route\n' + + ` matcher: '/((?!${tunnelPath.replace(/^\//, '')}|_next/static|_next/image|favicon.ico).*)',\n` + + ' };\n', ); showedMiddlewareMatcherWarning = true; } diff --git a/packages/nextjs/test/common/withSentryMiddlewareConfig.test.ts b/packages/nextjs/test/common/withSentryMiddlewareConfig.test.ts deleted file mode 100644 index 8399452b7723..000000000000 --- a/packages/nextjs/test/common/withSentryMiddlewareConfig.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { GLOBAL_OBJ } from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { withSentryMiddlewareConfig, withSentryProxyConfig } from '../../src/common/withSentryMiddlewareConfig'; - -const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { - _sentryRewritesTunnelPath?: string | null; -}; - -describe('withSentryMiddlewareConfig', () => { - let originalEnv: string | undefined; - let originalGlobal: unknown; - - beforeEach(() => { - // Save original values - originalEnv = process.env._sentryRewritesTunnelPath; - originalGlobal = globalWithInjectedValues._sentryRewritesTunnelPath; - }); - - afterEach(() => { - // Restore original values - if (originalEnv === undefined) { - delete process.env._sentryRewritesTunnelPath; - } else { - process.env._sentryRewritesTunnelPath = originalEnv; - } - - if (originalGlobal === undefined) { - delete globalWithInjectedValues._sentryRewritesTunnelPath; - } else { - // @ts-expect-error - we're resetting the value to the original value - globalWithInjectedValues._sentryRewritesTunnelPath = originalGlobal; - } - }); - - describe('when no tunnel path is configured', () => { - beforeEach(() => { - delete process.env._sentryRewritesTunnelPath; - delete globalWithInjectedValues._sentryRewritesTunnelPath; - }); - - it('should return config unchanged', () => { - const config = { matcher: ['/api/:path*', '/admin/:path*'] }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual(config); - }); - - it('should return config with no matcher unchanged', () => { - const config = { someOtherOption: true }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual(config); - }); - - it('should preserve other config properties', () => { - const config = { matcher: ['/api/:path*'], regions: ['us-east-1'], custom: true }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual(config); - }); - }); - - describe('when tunnel path is configured via process.env', () => { - beforeEach(() => { - process.env._sentryRewritesTunnelPath = '/sentry-tunnel'; - }); - - it('should add exclusion pattern to config with string matcher', () => { - const config = { matcher: '/api/:path*' }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/api/:path*', '/((?!sentry-tunnel).*)'], - }); - }); - - it('should add exclusion pattern to config with array matcher', () => { - const config = { matcher: ['/api/:path*', '/admin/:path*'] }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/api/:path*', '/admin/:path*', '/((?!sentry-tunnel).*)'], - }); - }); - - it('should handle tunnel path without leading slash', () => { - process.env._sentryRewritesTunnelPath = 'tunnel-route'; - const config = { matcher: '/api/:path*' }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/api/:path*', '/((?!tunnel-route).*)'], - }); - }); - - it('should handle tunnel path with leading slash', () => { - process.env._sentryRewritesTunnelPath = '/tunnel-route'; - const config = { matcher: '/api/:path*' }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/api/:path*', '/((?!tunnel-route).*)'], - }); - }); - - it('should work with random generated tunnel paths', () => { - process.env._sentryRewritesTunnelPath = '/abc123xyz'; - const config = { matcher: ['/api/:path*'] }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/api/:path*', '/((?!abc123xyz).*)'], - }); - }); - - it('should work with empty array matcher', () => { - const config = { matcher: [] }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/((?!sentry-tunnel).*)'], - }); - }); - - it('should preserve other config properties', () => { - const config = { matcher: ['/api/:path*'], regions: ['us-east-1'], custom: true }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/api/:path*', '/((?!sentry-tunnel).*)'], - regions: ['us-east-1'], - custom: true, - }); - }); - }); - - describe('when tunnel path is configured via GLOBAL_OBJ', () => { - beforeEach(() => { - delete process.env._sentryRewritesTunnelPath; - globalWithInjectedValues._sentryRewritesTunnelPath = '/global-tunnel'; - }); - - it('should add exclusion pattern using global value', () => { - const config = { matcher: '/api/:path*' }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/api/:path*', '/((?!global-tunnel).*)'], - }); - }); - - it('should prefer process.env over GLOBAL_OBJ', () => { - process.env._sentryRewritesTunnelPath = '/env-tunnel'; - const config = { matcher: '/api/:path*' }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/api/:path*', '/((?!env-tunnel).*)'], - }); - }); - }); - - describe('edge cases', () => { - beforeEach(() => { - process.env._sentryRewritesTunnelPath = '/tunnel'; - }); - - it('should handle single slash matcher', () => { - const config = { matcher: '/' }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/', '/((?!tunnel).*)'], - }); - }); - - it('should handle complex path patterns', () => { - const config = { - matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)', '/api/protected/:path*'], - }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)', '/api/protected/:path*', '/((?!tunnel).*)'], - }); - }); - - it('should handle matcher with special regex characters in tunnel path', () => { - process.env._sentryRewritesTunnelPath = '/tunnel-route-123'; - const config = { matcher: '/api/:path*' }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/api/:path*', '/((?!tunnel-route-123).*)'], - }); - }); - }); - - describe('real-world usage patterns', () => { - beforeEach(() => { - process.env._sentryRewritesTunnelPath = '/monitoring'; - }); - - it('should work with typical API route matchers', () => { - const config = { matcher: ['/api/:path*', '/trpc/:path*'] }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/api/:path*', '/trpc/:path*', '/((?!monitoring).*)'], - }); - }); - - it('should work with exclusion-based matchers', () => { - const config = { matcher: '/((?!_next/static|_next/image|favicon.ico).*)' }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/((?!_next/static|_next/image|favicon.ico).*)', '/((?!monitoring).*)'], - }); - }); - - it('should work with admin and protected routes', () => { - const config = { matcher: ['/admin/:path*', '/dashboard/:path*', '/api/auth/:path*'] }; - const result = withSentryMiddlewareConfig(config); - expect(result).toEqual({ - matcher: ['/admin/:path*', '/dashboard/:path*', '/api/auth/:path*', '/((?!monitoring).*)'], - }); - }); - }); - - describe('withSentryProxyConfig alias', () => { - beforeEach(() => { - process.env._sentryRewritesTunnelPath = '/sentry-tunnel'; - }); - - it('should be an alias for withSentryMiddlewareConfig', () => { - expect(withSentryProxyConfig).toBe(withSentryMiddlewareConfig); - }); - - it('should work identically to withSentryMiddlewareConfig', () => { - const config = { matcher: ['/api/:path*'] }; - const resultMiddleware = withSentryMiddlewareConfig(config); - const resultProxy = withSentryProxyConfig(config); - expect(resultProxy).toEqual(resultMiddleware); - }); - }); -}); From 856a4d1077935c9e61ed380bbb5a510bf872c5a0 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 21 Nov 2025 12:55:41 +0200 Subject: [PATCH 10/16] ref: remove warning, no point if they cannot do anything about it --- packages/nextjs/src/config/util.ts | 34 ----- .../nextjs/src/config/withSentryConfig.ts | 42 ------ packages/nextjs/test/config/util.test.ts | 134 +----------------- 3 files changed, 1 insertion(+), 209 deletions(-) diff --git a/packages/nextjs/src/config/util.ts b/packages/nextjs/src/config/util.ts index 277ba3660713..0d4a55687d2f 100644 --- a/packages/nextjs/src/config/util.ts +++ b/packages/nextjs/src/config/util.ts @@ -181,37 +181,3 @@ export function detectActiveBundler(): 'turbopack' | 'webpack' { return 'webpack'; } } - -/** - * Finds the middleware or proxy file in the Next.js project. - * Next.js only allows one middleware file, so this returns the first match. - */ -export function findMiddlewareFile(): { path: string; contents: string } | undefined { - const projectDir = process.cwd(); - - // In Next.js 16+, the file is called 'proxy', in earlier versions it's 'middleware' - const nextVersion = getNextjsVersion(); - const nextMajor = nextVersion ? parseSemver(nextVersion).major : undefined; - const basename = nextMajor && nextMajor >= 16 ? 'proxy' : 'middleware'; - const directories = [projectDir, `${projectDir}/src`]; - const extensions = ['.ts', '.js']; - - // Find the first existing middleware/proxy file - for (const dir of directories) { - for (const ext of extensions) { - const filePath = `${dir}/${basename}${ext}`; - if (fs.existsSync(filePath)) { - try { - const contents = fs.readFileSync(filePath, 'utf-8'); - - return { path: filePath, contents }; - } catch { - // If we can't read the file, continue searching - continue; - } - } - } - } - - return undefined; -} diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 18e1d2e90352..43455f06f54d 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -18,7 +18,6 @@ import type { } from './types'; import { detectActiveBundler, - findMiddlewareFile, getNextjsVersion, requiresInstrumentationHook, supportsProductionCompileHook, @@ -27,7 +26,6 @@ import { constructWebpackConfigFunction } from './webpack'; let showedExportModeTunnelWarning = false; let showedExperimentalBuildModeWarning = false; -let showedMiddlewareMatcherWarning = false; // Packages we auto-instrument need to be external for instrumentation to work // Next.js externalizes some packages by default, see: https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages @@ -91,45 +89,6 @@ export function withSentryConfig(nextConfig?: C, sentryBuildOptions: SentryBu } } -/** - * Checks if the user has a middleware/proxy file with a matcher that might exclude the tunnel route. - * Warns the user if their matcher might interfere with the tunnel route. - */ -function checkMiddlewareMatcherForTunnelRoute(tunnelPath: string): void { - if (showedMiddlewareMatcherWarning) { - return; - } - - try { - const middlewareFile = findMiddlewareFile(); - - // No middleware file found - if (!middlewareFile) { - return; - } - - // Look for config.matcher export - const isProxy = middlewareFile.path.includes('proxy'); - const hasConfigMatcher = /export\s+const\s+config\s*=\s*{[^}]*matcher\s*:/s.test(middlewareFile.contents); - - if (hasConfigMatcher) { - // eslint-disable-next-line no-console - console.warn( - `[@sentry/nextjs] WARNING: You have a ${isProxy ? 'proxy' : 'middleware'} file (${path.basename(middlewareFile.path)}) with a \`config.matcher\`. ` + - `If your matcher runs on the Sentry tunnel route (${tunnelPath}), it may interfere with event delivery. ` + - 'Please ensure your matcher excludes the tunnel route. For example:\n\n' + - ' export const config = {\n' + - ' // Use a negative lookahead to exclude the Sentry tunnel route\n' + - ` matcher: '/((?!${tunnelPath.replace(/^\//, '')}|_next/static|_next/image|favicon.ico).*)',\n` + - ' };\n', - ); - showedMiddlewareMatcherWarning = true; - } - } catch { - // Silently fail - this is just a helpful warning, not critical - } -} - /** * Generates a random tunnel route path that's less likely to be blocked by ad-blockers */ @@ -167,7 +126,6 @@ function getFinalConfigObject( userSentryOptions.tunnelRoute = resolvedTunnelRoute || undefined; setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute); - checkMiddlewareMatcherForTunnelRoute(resolvedTunnelRoute); } } diff --git a/packages/nextjs/test/config/util.test.ts b/packages/nextjs/test/config/util.test.ts index 6f1f849268c3..7335139b5037 100644 --- a/packages/nextjs/test/config/util.test.ts +++ b/packages/nextjs/test/config/util.test.ts @@ -1,6 +1,4 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import * as util from '../../src/config/util'; describe('util', () => { @@ -336,134 +334,4 @@ describe('util', () => { expect(util.detectActiveBundler()).toBe('webpack'); }); }); - - describe('findMiddlewareFile', () => { - vi.mock('../../src/config/util', async () => { - const actual = await vi.importActual('../../src/config/util'); - return { - ...actual, - getNextjsVersion: vi.fn(), - }; - }); - - let originalCwd: string; - let testDir: string; - - beforeEach(() => { - originalCwd = process.cwd(); - testDir = path.join(__dirname, '.test-middleware-temp'); - - // Create test directory - if (!fs.existsSync(testDir)) { - fs.mkdirSync(testDir, { recursive: true }); - } - - process.chdir(testDir); - }); - - afterEach(() => { - process.chdir(originalCwd); - - // Clean up test directory - if (fs.existsSync(testDir)) { - fs.rmSync(testDir, { recursive: true, force: true }); - } - - vi.clearAllMocks(); - }); - - describe('Next.js <16 (middleware)', () => { - beforeEach(() => { - vi.mocked(util.getNextjsVersion).mockReturnValue('15.0.0'); - }); - - it('should find middleware.ts in root directory', () => { - fs.writeFileSync(path.join(testDir, 'middleware.ts'), 'export default function middleware() {}'); - - const result = util.findMiddlewareFile(); - - expect(result).toBeDefined(); - expect(result?.path).toContain('middleware.ts'); - expect(result?.contents).toBe('export default function middleware() {}'); - }); - - it('should find middleware.js in root directory', () => { - fs.writeFileSync(path.join(testDir, 'middleware.js'), 'module.exports = function middleware() {}'); - - const result = util.findMiddlewareFile(); - - expect(result).toBeDefined(); - expect(result?.path).toContain('middleware.js'); - expect(result?.contents).toBe('module.exports = function middleware() {}'); - }); - - it('should find middleware.ts in src directory', () => { - fs.mkdirSync(path.join(testDir, 'src'), { recursive: true }); - fs.writeFileSync(path.join(testDir, 'src', 'middleware.ts'), 'export default function middleware() {}'); - - const result = util.findMiddlewareFile(); - - expect(result).toBeDefined(); - expect(result?.path).toContain(path.join('src', 'middleware.ts')); - expect(result?.contents).toBe('export default function middleware() {}'); - }); - - it('should prefer root over src directory', () => { - fs.mkdirSync(path.join(testDir, 'src'), { recursive: true }); - fs.writeFileSync(path.join(testDir, 'middleware.ts'), 'root middleware'); - fs.writeFileSync(path.join(testDir, 'src', 'middleware.ts'), 'src middleware'); - - const result = util.findMiddlewareFile(); - - expect(result).toBeDefined(); - expect(result?.contents).toBe('root middleware'); - }); - - it('should prefer .ts over .js extension', () => { - fs.writeFileSync(path.join(testDir, 'middleware.ts'), 'typescript middleware'); - fs.writeFileSync(path.join(testDir, 'middleware.js'), 'javascript middleware'); - - const result = util.findMiddlewareFile(); - - expect(result).toBeDefined(); - expect(result?.contents).toBe('typescript middleware'); - }); - - it('should NOT find proxy.ts on Next.js <16', () => { - fs.writeFileSync(path.join(testDir, 'proxy.ts'), 'export default function proxy() {}'); - - const result = util.findMiddlewareFile(); - - expect(result).toBeUndefined(); - }); - - it('should return undefined when no middleware file exists', () => { - const result = util.findMiddlewareFile(); - - expect(result).toBeUndefined(); - }); - }); - - // Note: Tests for Next.js 16+ (proxy) behavior would require mocking getNextjsVersion, - // which is challenging in this test setup due to module imports. - // The logic is the same as middleware tests but with 'proxy' instead of 'middleware'. - - describe('edge cases', () => { - beforeEach(() => { - vi.mocked(util.getNextjsVersion).mockReturnValue('15.0.0'); - }); - - it('should handle when getNextjsVersion returns undefined', () => { - vi.mocked(util.getNextjsVersion).mockReturnValue(undefined); - - fs.writeFileSync(path.join(testDir, 'middleware.ts'), 'export default function middleware() {}'); - - const result = util.findMiddlewareFile(); - - // Should default to 'middleware' when version is unknown - expect(result).toBeDefined(); - expect(result?.path).toContain('middleware.ts'); - }); - }); - }); }); From d2c11a095bdea34d54e10873538c17e2558b2db8 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 21 Nov 2025 14:38:25 +0200 Subject: [PATCH 11/16] test: added internal config for testing --- packages/nextjs/src/config/types.ts | 6 ++++++ packages/nextjs/src/config/withSentryConfig.ts | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 28e038b6d0f2..4e7bb6ce6438 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -437,6 +437,12 @@ export type SentryBuildOptions = { */ tunnelRoute?: string | boolean; + /** + * @internal + * Override the destination URL for tunnel rewrites (for E2E testing only) + */ + _tunnelRouteDestinationOverride?: string; + /** * Tree shakes Sentry SDK logger statements from the bundle. */ diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 43455f06f54d..84607ba7e385 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -125,7 +125,11 @@ function getFinalConfigObject( const resolvedTunnelRoute = resolveTunnelRoute(userSentryOptions.tunnelRoute); userSentryOptions.tunnelRoute = resolvedTunnelRoute || undefined; - setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute); + setUpTunnelRewriteRules( + incomingUserNextConfigObject, + resolvedTunnelRoute, + userSentryOptions._tunnelRouteDestinationOverride, + ); } } @@ -389,7 +393,11 @@ function getFinalConfigObject( * * See https://nextjs.org/docs/api-reference/next.config.js/rewrites. */ -function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: string): void { +function setUpTunnelRewriteRules( + userNextConfig: NextConfigObject, + tunnelPath: string, + destinationOverride?: string, +): void { const originalRewrites = userNextConfig.rewrites; // This function doesn't take any arguments at the time of writing but we future-proof @@ -411,7 +419,7 @@ function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: s value: '(?\\d*)', }, ], - destination: 'https://o:orgid.ingest.sentry.io/api/:projectid/envelope/?hsts=0', + destination: destinationOverride || 'https://o:orgid.ingest.sentry.io/api/:projectid/envelope/?hsts=0', }; const tunnelRouteRewriteWithRegion = { @@ -435,7 +443,7 @@ function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: s value: '(?[a-z]{2})', }, ], - destination: 'https://o:orgid.ingest.:region.sentry.io/api/:projectid/envelope/?hsts=0', + destination: destinationOverride || 'https://o:orgid.ingest.:region.sentry.io/api/:projectid/envelope/?hsts=0', }; // Order of these is important, they get applied first to last. From 1c1ab8753729699a03b8e7c82ab34c26b4175b35 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 21 Nov 2025 14:38:49 +0200 Subject: [PATCH 12/16] test: remove pointless test --- .../test/config/tunnelRouteCaching.test.ts | 90 ------------------- 1 file changed, 90 deletions(-) delete mode 100644 packages/nextjs/test/config/tunnelRouteCaching.test.ts diff --git a/packages/nextjs/test/config/tunnelRouteCaching.test.ts b/packages/nextjs/test/config/tunnelRouteCaching.test.ts deleted file mode 100644 index 4c5fc300c290..000000000000 --- a/packages/nextjs/test/config/tunnelRouteCaching.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; - -describe('Tunnel Route Caching (Environment Variable)', () => { - let originalNextPhase: string | undefined; - let originalTunnelRoute: string | undefined; - - beforeEach(() => { - // Save and clear env vars - originalNextPhase = process.env.NEXT_PHASE; - originalTunnelRoute = process.env.__SENTRY_TUNNEL_ROUTE__; - delete process.env.__SENTRY_TUNNEL_ROUTE__; - }); - - afterEach(() => { - // Restore env vars - if (originalNextPhase !== undefined) { - process.env.NEXT_PHASE = originalNextPhase; - } else { - delete process.env.NEXT_PHASE; - } - - if (originalTunnelRoute !== undefined) { - process.env.__SENTRY_TUNNEL_ROUTE__ = originalTunnelRoute; - } else { - delete process.env.__SENTRY_TUNNEL_ROUTE__; - } - }); - - it('caches tunnel route in environment variable during build phase', () => { - process.env.NEXT_PHASE = 'phase-production-build'; - process.env.__SENTRY_TUNNEL_ROUTE__ = '/cached-route-123'; - - // The env var should be accessible - expect(process.env.__SENTRY_TUNNEL_ROUTE__).toBe('/cached-route-123'); - }); - - it('environment variable persists across different contexts', () => { - process.env.NEXT_PHASE = 'phase-production-build'; - process.env.__SENTRY_TUNNEL_ROUTE__ = '/test-route-456'; - - // Simulate accessing from different module - const cachedRoute = process.env.__SENTRY_TUNNEL_ROUTE__; - - expect(cachedRoute).toBe('/test-route-456'); - }); - - it('verifies NEXT_PHASE detection for build time', () => { - process.env.NEXT_PHASE = 'phase-production-build'; - - const isBuildTime = process.env.NEXT_PHASE === 'phase-production-build'; - - expect(isBuildTime).toBe(true); - }); - - it('verifies NEXT_PHASE detection for non-build time', () => { - process.env.NEXT_PHASE = 'phase-development-server'; - - const isBuildTime = process.env.NEXT_PHASE === 'phase-production-build'; - - expect(isBuildTime).toBe(false); - }); - - it('handles missing NEXT_PHASE', () => { - delete process.env.NEXT_PHASE; - - const isBuildTime = process.env.NEXT_PHASE === 'phase-production-build'; - - expect(isBuildTime).toBe(false); - }); -}); - -describe('Random Tunnel Route Generation', () => { - it('generates an 8-character alphanumeric string', () => { - const randomString = Math.random().toString(36).substring(2, 10); - const tunnelRoute = `/${randomString}`; - - // Should be a path with 8 alphanumeric chars - expect(tunnelRoute).toMatch(/^\/[a-z0-9]{8}$/); - }); - - it('generates different values on multiple calls', () => { - const route1 = `/${Math.random().toString(36).substring(2, 10)}`; - const route2 = `/${Math.random().toString(36).substring(2, 10)}`; - - // Very unlikely to be the same (but not impossible) - // This is more of a sanity check - expect(route1).toMatch(/^\/[a-z0-9]{8}$/); - expect(route2).toMatch(/^\/[a-z0-9]{8}$/); - }); -}); From 5574e2d0d63db47b121118a5cd0c273733a90c9a Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 21 Nov 2025 14:40:14 +0200 Subject: [PATCH 13/16] test: added e2e tests --- .../nextjs-16-tunnel/.gitignore | 46 +++++ .../test-applications/nextjs-16-tunnel/.npmrc | 4 + .../nextjs-16-tunnel/app/favicon.ico | Bin 0 -> 25931 bytes .../nextjs-16-tunnel/app/global-error.tsx | 23 +++ .../nextjs-16-tunnel/app/layout.tsx | 7 + .../nextjs-16-tunnel/app/page.tsx | 3 + .../nextjs-16-tunnel/eslint.config.mjs | 19 ++ .../instrumentation-client.ts | 12 ++ .../nextjs-16-tunnel/instrumentation.ts | 13 ++ .../nextjs-16-tunnel/next.config.ts | 10 ++ .../nextjs-16-tunnel/package.json | 63 +++++++ .../nextjs-16-tunnel/playwright.config.mjs | 29 +++ .../nextjs-16-tunnel/proxy.ts | 11 ++ .../nextjs-16-tunnel/public/file.svg | 1 + .../nextjs-16-tunnel/public/globe.svg | 1 + .../nextjs-16-tunnel/public/next.svg | 1 + .../nextjs-16-tunnel/public/vercel.svg | 1 + .../nextjs-16-tunnel/public/window.svg | 1 + .../nextjs-16-tunnel/sentry.edge.config.ts | 11 ++ .../nextjs-16-tunnel/sentry.server.config.ts | 11 ++ .../nextjs-16-tunnel/start-event-proxy.mjs | 14 ++ .../tests/tunnel-route.test.ts | 169 ++++++++++++++++++ .../nextjs-16-tunnel/tsconfig.json | 27 +++ 23 files changed, 477 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/favicon.ico create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/global-error.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/eslint.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation-client.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/proxy.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/file.svg create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/globe.svg create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/next.svg create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/vercel.svg create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/window.svg create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.edge.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.server.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.gitignore new file mode 100644 index 000000000000..ae044ec5ad53 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.gitignore @@ -0,0 +1,46 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +event-dumps + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# Sentry Config File +.env.sentry-build-plugin diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.npmrc new file mode 100644 index 000000000000..a3160f4de175 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.npmrc @@ -0,0 +1,4 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 +public-hoist-pattern[]=*import-in-the-middle* +public-hoist-pattern[]=*require-in-the-middle* diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/favicon.ico b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/global-error.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/global-error.tsx new file mode 100644 index 000000000000..20c175015b03 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/global-error.tsx @@ -0,0 +1,23 @@ +'use client'; + +import * as Sentry from '@sentry/nextjs'; +import NextError from 'next/error'; +import { useEffect } from 'react'; + +export default function GlobalError({ error }: { error: Error & { digest?: string } }) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + {/* `NextError` is the default Next.js error page component. Its type + definition requires a `statusCode` prop. However, since the App Router + does not expose status codes for errors, we simply pass 0 to render a + generic error message. */} + + + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/layout.tsx new file mode 100644 index 000000000000..c8f9cee0b787 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/layout.tsx @@ -0,0 +1,7 @@ +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/page.tsx new file mode 100644 index 000000000000..f28a670096bf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

Next.js 16 Tunnel Route Test

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/eslint.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/eslint.config.mjs new file mode 100644 index 000000000000..60f7af38f6c2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/eslint.config.mjs @@ -0,0 +1,19 @@ +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { FlatCompat } from '@eslint/eslintrc'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends('next/core-web-vitals', 'next/typescript'), + { + ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'], + }, +]; + +export default eslintConfig; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation-client.ts new file mode 100644 index 000000000000..d40b790f18a5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation-client.ts @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + // Use a fake but properly formatted Sentry SaaS DSN for tunnel route testing + dsn: 'https://public@o12345.ingest.us.sentry.io/67890', + // No tunnel option - using tunnelRoute from withSentryConfig + tracesSampleRate: 1.0, + sendDefaultPii: true, +}); + +export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation.ts new file mode 100644 index 000000000000..964f937c439a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/instrumentation.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/nextjs'; + +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + await import('./sentry.server.config'); + } + + if (process.env.NEXT_RUNTIME === 'edge') { + await import('./sentry.edge.config'); + } +} + +export const onRequestError = Sentry.captureRequestError; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts new file mode 100644 index 000000000000..710b47e2a100 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts @@ -0,0 +1,10 @@ +import { withSentryConfig } from '@sentry/nextjs'; +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = {}; + +export default withSentryConfig(nextConfig, { + silent: true, + tunnelRoute: true, + _tunnelRouteDestinationOverride: 'http://localhost:3031/', +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json new file mode 100644 index 000000000000..22a6d8e693f3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json @@ -0,0 +1,63 @@ +{ + "name": "nextjs-16-tunnel", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", + "clean": "npx rimraf node_modules pnpm-lock.yaml .tmp_dev_server_logs", + "dev:webpack": "next dev --webpack", + "build-webpack": "next build --webpack > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", + "start": "next start", + "lint": "eslint", + "test:prod": "TEST_ENV=production playwright test", + "test:dev": "TEST_ENV=development playwright test", + "test:dev-webpack": "TEST_ENV=development-webpack playwright test", + "test:build": "pnpm install && pnpm build", + "test:build-webpack": "pnpm install && pnpm build-webpack", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@latest && pnpm build", + "test:build-latest-webpack": "pnpm install && pnpm add next@latest && pnpm build-webpack", + "test:build-canary-webpack": "pnpm install && pnpm add next@canary && pnpm build-webpack", + "test:assert": "pnpm test:prod && pnpm test:dev", + "test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack" + }, + "dependencies": { + "@sentry/nextjs": "latest || *", + "@sentry/core": "latest || *", + "ai": "^3.0.0", + "import-in-the-middle": "^1", + "next": "16.0.0", + "react": "19.1.0", + "react-dom": "19.1.0", + "require-in-the-middle": "^7", + "zod": "^3.22.4" + }, + "devDependencies": { + "@playwright/test": "~1.53.2", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "canary", + "typescript": "^5" + }, + "volta": { + "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "pnpm test:build-webpack", + "label": "nextjs-16-tunnel (webpack)", + "assert-command": "pnpm test:assert-webpack" + }, + { + "build-command": "pnpm test:build", + "label": "nextjs-16-tunnel (turbopack)", + "assert-command": "pnpm test:assert" + } + ] + } +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/playwright.config.mjs new file mode 100644 index 000000000000..797418b8cf7d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/playwright.config.mjs @@ -0,0 +1,29 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const getStartCommand = () => { + if (testEnv === 'development-webpack') { + return 'pnpm next dev -p 3030 --webpack 2>&1 | tee .tmp_dev_server_logs'; + } + + if (testEnv === 'development') { + return 'pnpm next dev -p 3030 2>&1 | tee .tmp_dev_server_logs'; + } + + if (testEnv === 'production') { + return 'pnpm next start -p 3030'; + } + + throw new Error(`Unknown test env: ${testEnv}`); +}; + +const config = getPlaywrightConfig({ + startCommand: getStartCommand(), + port: 3030, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/proxy.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/proxy.ts new file mode 100644 index 000000000000..28639f60bbe4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/proxy.ts @@ -0,0 +1,11 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +export async function proxy(_request: NextRequest) { + return NextResponse.next(); +} + +// Match all routes to test that tunnel requests are properly filtered +export const config = { + matcher: '/:path*', +}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/file.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/file.svg new file mode 100644 index 000000000000..004145cddf3f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/globe.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/globe.svg new file mode 100644 index 000000000000..567f17b0d7c7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/next.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/next.svg new file mode 100644 index 000000000000..5174b28c565c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/vercel.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/vercel.svg new file mode 100644 index 000000000000..77053960334e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/window.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/window.svg new file mode 100644 index 000000000000..b2b2a44f6ebc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.edge.config.ts new file mode 100644 index 000000000000..8ba3a3bf2faa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.edge.config.ts @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + // Use a fake but properly formatted Sentry SaaS DSN for tunnel route testing + dsn: 'https://public@o12345.ingest.us.sentry.io/67890', + // No tunnel option - using tunnelRoute from withSentryConfig + tracesSampleRate: 1.0, + sendDefaultPii: true, + // debug: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.server.config.ts new file mode 100644 index 000000000000..8ba3a3bf2faa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.server.config.ts @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + // Use a fake but properly formatted Sentry SaaS DSN for tunnel route testing + dsn: 'https://public@o12345.ingest.us.sentry.io/67890', + // No tunnel option - using tunnelRoute from withSentryConfig + tracesSampleRate: 1.0, + sendDefaultPii: true, + // debug: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/start-event-proxy.mjs new file mode 100644 index 000000000000..976073d3d2c4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/start-event-proxy.mjs @@ -0,0 +1,14 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'))); + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nextjs-16-tunnel', + envelopeDumpPath: path.join( + process.cwd(), + `event-dumps/nextjs-16-tunnel-v${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`, + ), +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts new file mode 100644 index 000000000000..83d9a81d55f2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts @@ -0,0 +1,169 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Tunnel route should proxy pageload transaction to Sentry', async ({ page }) => { + // Wait for the pageload transaction to be sent through the tunnel + const pageloadTransactionPromise = waitForTransaction('nextjs-16-tunnel', async transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; + }); + + // Navigate to the page + await page.goto('/'); + + const pageloadTransaction = await pageloadTransactionPromise; + + // Verify the pageload transaction was received successfully + expect(pageloadTransaction).toBeDefined(); + expect(pageloadTransaction.transaction).toBe('/'); + expect(pageloadTransaction.contexts?.trace?.op).toBe('pageload'); + expect(pageloadTransaction.contexts?.trace?.status).toBe('ok'); + expect(pageloadTransaction.type).toBe('transaction'); +}); + +test('Tunnel route should send multiple pageload transactions consistently', async ({ page }) => { + // This test verifies that the tunnel route remains consistent across multiple page loads + // (important for Turbopack which could generate different tunnel routes for client/server) + + // First pageload + const firstPageloadPromise = waitForTransaction('nextjs-16-tunnel', async transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; + }); + + await page.goto('/'); + const firstPageload = await firstPageloadPromise; + + expect(firstPageload).toBeDefined(); + expect(firstPageload.transaction).toBe('/'); + expect(firstPageload.contexts?.trace?.op).toBe('pageload'); + expect(firstPageload.contexts?.trace?.status).toBe('ok'); + + // Second pageload (reload) + const secondPageloadPromise = waitForTransaction('nextjs-16-tunnel', async transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; + }); + + await page.reload(); + const secondPageload = await secondPageloadPromise; + + expect(secondPageload).toBeDefined(); + expect(secondPageload.transaction).toBe('/'); + expect(secondPageload.contexts?.trace?.op).toBe('pageload'); + expect(secondPageload.contexts?.trace?.status).toBe('ok'); +}); + +test('Tunnel requests should not create middleware or fetch spans', async ({ page }) => { + // This test verifies that our span filtering logic works correctly + // The proxy runs on all routes, so we'll get a middleware transaction for `/` + // But we should NOT get middleware or fetch transactions for the tunnel route itself + + const allTransactions: any[] = []; + + // Collect all transactions + const collectPromise = (async () => { + // Keep collecting for 3 seconds after pageload + const endTime = Date.now() + 3000; + while (Date.now() < endTime) { + try { + const tx = await Promise.race([ + waitForTransaction('nextjs-16-tunnel', () => true), + new Promise((_, reject) => setTimeout(() => reject(), 500)), + ]); + allTransactions.push(tx); + } catch { + // Timeout, continue collecting + } + } + })(); + + // Wait for pageload transaction + const pageloadPromise = waitForTransaction('nextjs-16-tunnel', async transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + const pageloadTransaction = await pageloadPromise; + + // Trigger errors to force tunnel POST requests + await page + .evaluate(() => { + throw new Error('Test tunnel error 1'); + }) + .catch(() => { + // Expected to throw + }); + + await page + .evaluate(() => { + throw new Error('Test tunnel error 2'); + }) + .catch(() => { + // Expected to throw + }); + + // Wait for events to be sent through tunnel + await page.waitForTimeout(2000); + + // Continue collecting for a bit + await collectPromise; + + // Log all transactions we received with full details + console.log('=== All transactions received ==='); + allTransactions.forEach(tx => { + console.log({ + transaction: tx.transaction, + op: tx.contexts?.trace?.op, + type: tx.type, + method: tx.contexts?.trace?.data?.['http.request.method'] || tx.request?.method, + url: tx.contexts?.trace?.data?.['url.full'] || tx.request?.url, + status: tx.contexts?.trace?.status, + trace: tx.contexts?.trace, + }); + }); + + // We should have received the pageload transaction + expect(pageloadTransaction).toBeDefined(); + expect(pageloadTransaction.contexts?.trace?.op).toBe('pageload'); + + // Log all middleware transactions to see what methods they use + const middlewareTransactions = allTransactions.filter(tx => tx.contexts?.trace?.op === 'http.server.middleware'); + + console.log('=== All middleware transactions ===', middlewareTransactions.length); + middlewareTransactions.forEach(tx => { + console.log({ + transaction: tx.transaction, + op: tx.contexts?.trace?.op, + method: tx.contexts?.trace?.data?.['http.request.method'], + target: tx.contexts?.trace?.data?.['http.target'], + data: tx.contexts?.trace?.data, + }); + }); + + // We WILL have a middleware transaction for GET / (the pageload) + // But we should NOT have middleware transactions for POST requests (tunnel route) + const postMiddlewareTransactions = middlewareTransactions.filter( + tx => tx.transaction?.includes('POST') || tx.contexts?.trace?.data?.['http.request.method'] === 'POST', + ); + + console.log('=== POST middleware transactions ===', postMiddlewareTransactions.length); + + expect(postMiddlewareTransactions).toHaveLength(0); + + // We should NOT have any fetch transactions to Sentry ingest + const sentryFetchTransactions = allTransactions.filter( + tx => + tx.contexts?.trace?.op === 'http.client' && + (tx.contexts?.trace?.data?.['url.full']?.includes('sentry.io') || + tx.contexts?.trace?.data?.['url.full']?.includes('ingest')), + ); + + console.log('=== Sentry fetch transactions ===', sentryFetchTransactions.length); + sentryFetchTransactions.forEach(tx => { + console.log({ + transaction: tx.transaction, + op: tx.contexts?.trace?.op, + url: tx.contexts?.trace?.data?.['url.full'], + }); + }); + + expect(sentryFetchTransactions).toHaveLength(0); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tsconfig.json new file mode 100644 index 000000000000..cc9ed39b5aa2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts"], + "exclude": ["node_modules"] +} From 4171ecad208d5caf69695eee5b2415b4282fd9c6 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 21 Nov 2025 14:46:44 +0200 Subject: [PATCH 14/16] fix: remove logs --- .../tests/tunnel-route.test.ts | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts index 83d9a81d55f2..a8bd7b4d925e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts @@ -106,46 +106,18 @@ test('Tunnel requests should not create middleware or fetch spans', async ({ pag // Continue collecting for a bit await collectPromise; - // Log all transactions we received with full details - console.log('=== All transactions received ==='); - allTransactions.forEach(tx => { - console.log({ - transaction: tx.transaction, - op: tx.contexts?.trace?.op, - type: tx.type, - method: tx.contexts?.trace?.data?.['http.request.method'] || tx.request?.method, - url: tx.contexts?.trace?.data?.['url.full'] || tx.request?.url, - status: tx.contexts?.trace?.status, - trace: tx.contexts?.trace, - }); - }); - // We should have received the pageload transaction expect(pageloadTransaction).toBeDefined(); expect(pageloadTransaction.contexts?.trace?.op).toBe('pageload'); - // Log all middleware transactions to see what methods they use const middlewareTransactions = allTransactions.filter(tx => tx.contexts?.trace?.op === 'http.server.middleware'); - console.log('=== All middleware transactions ===', middlewareTransactions.length); - middlewareTransactions.forEach(tx => { - console.log({ - transaction: tx.transaction, - op: tx.contexts?.trace?.op, - method: tx.contexts?.trace?.data?.['http.request.method'], - target: tx.contexts?.trace?.data?.['http.target'], - data: tx.contexts?.trace?.data, - }); - }); - // We WILL have a middleware transaction for GET / (the pageload) // But we should NOT have middleware transactions for POST requests (tunnel route) const postMiddlewareTransactions = middlewareTransactions.filter( tx => tx.transaction?.includes('POST') || tx.contexts?.trace?.data?.['http.request.method'] === 'POST', ); - console.log('=== POST middleware transactions ===', postMiddlewareTransactions.length); - expect(postMiddlewareTransactions).toHaveLength(0); // We should NOT have any fetch transactions to Sentry ingest @@ -156,14 +128,5 @@ test('Tunnel requests should not create middleware or fetch spans', async ({ pag tx.contexts?.trace?.data?.['url.full']?.includes('ingest')), ); - console.log('=== Sentry fetch transactions ===', sentryFetchTransactions.length); - sentryFetchTransactions.forEach(tx => { - console.log({ - transaction: tx.transaction, - op: tx.contexts?.trace?.op, - url: tx.contexts?.trace?.data?.['url.full'], - }); - }); - expect(sentryFetchTransactions).toHaveLength(0); }); From 981121c70e57fdbecd3493262b46de232dc5962e Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 21 Nov 2025 15:05:54 +0200 Subject: [PATCH 15/16] qef: use env for tests instead --- .../nextjs-16-tunnel/next.config.ts | 1 - .../nextjs-16-tunnel/package.json | 14 +++++------ .../utils/dropMiddlewareTunnelRequests.ts | 4 ++-- packages/nextjs/src/config/types.ts | 6 ----- .../nextjs/src/config/withSentryConfig.ts | 23 +++++++++---------- 5 files changed, 20 insertions(+), 28 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts index 710b47e2a100..cad68b926a58 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/next.config.ts @@ -6,5 +6,4 @@ const nextConfig: NextConfig = {}; export default withSentryConfig(nextConfig, { silent: true, tunnelRoute: true, - _tunnelRouteDestinationOverride: 'http://localhost:3031/', }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json index 22a6d8e693f3..40389ad0888f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json @@ -3,16 +3,16 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", - "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", + "dev": " next dev", + "build": "_SENTRY_TUNNEL_DESTINATION_OVERRIDE=http://localhost:3031/ next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", "clean": "npx rimraf node_modules pnpm-lock.yaml .tmp_dev_server_logs", - "dev:webpack": "next dev --webpack", - "build-webpack": "next build --webpack > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", - "start": "next start", + "dev:webpack": "_SENTRY_TUNNEL_DESTINATION_OVERRIDE=http://localhost:3031/ next dev --webpack", + "build-webpack": "_SENTRY_TUNNEL_DESTINATION_OVERRIDE=http://localhost:3031/ next build --webpack > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", + "start": "_SENTRY_TUNNEL_DESTINATION_OVERRIDE=http://localhost:3031/ next start", "lint": "eslint", "test:prod": "TEST_ENV=production playwright test", - "test:dev": "TEST_ENV=development playwright test", - "test:dev-webpack": "TEST_ENV=development-webpack playwright test", + "test:dev": "TEST_ENV=development _SENTRY_TUNNEL_DESTINATION_OVERRIDE=http://localhost:3031/ playwright test", + "test:dev-webpack": "TEST_ENV=development-webpack _SENTRY_TUNNEL_DESTINATION_OVERRIDE=http://localhost:3031/ playwright test", "test:build": "pnpm install && pnpm build", "test:build-webpack": "pnpm install && pnpm build-webpack", "test:build-canary": "pnpm install && pnpm add next@canary && pnpm build", diff --git a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts index 476298d37f56..b98711acb3a0 100644 --- a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts +++ b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts @@ -1,4 +1,4 @@ -import { ATTR_URL_QUERY, SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions'; +import { SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions'; import { type Span, type SpanAttributes, GLOBAL_OBJ, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { isSentryRequestSpan } from '@sentry/opentelemetry'; import { ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes'; @@ -49,7 +49,7 @@ function isTunnelRouteSpan(spanAttributes: Record): boolean { // Check both http.target (older) and url.query (newer) attributes // eslint-disable-next-line deprecation/deprecation - const httpTarget = spanAttributes[SEMATTRS_HTTP_TARGET] || spanAttributes[ATTR_URL_QUERY]; + const httpTarget = spanAttributes[SEMATTRS_HTTP_TARGET]; if (typeof httpTarget === 'string') { // Extract pathname from the target (e.g., "/tunnel?o=123&p=456" -> "/tunnel") diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 4e7bb6ce6438..28e038b6d0f2 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -437,12 +437,6 @@ export type SentryBuildOptions = { */ tunnelRoute?: string | boolean; - /** - * @internal - * Override the destination URL for tunnel rewrites (for E2E testing only) - */ - _tunnelRouteDestinationOverride?: string; - /** * Tree shakes Sentry SDK logger statements from the bundle. */ diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 84607ba7e385..892f4d6745fa 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -125,11 +125,7 @@ function getFinalConfigObject( const resolvedTunnelRoute = resolveTunnelRoute(userSentryOptions.tunnelRoute); userSentryOptions.tunnelRoute = resolvedTunnelRoute || undefined; - setUpTunnelRewriteRules( - incomingUserNextConfigObject, - resolvedTunnelRoute, - userSentryOptions._tunnelRouteDestinationOverride, - ); + setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute); } } @@ -393,12 +389,15 @@ function getFinalConfigObject( * * See https://nextjs.org/docs/api-reference/next.config.js/rewrites. */ -function setUpTunnelRewriteRules( - userNextConfig: NextConfigObject, - tunnelPath: string, - destinationOverride?: string, -): void { +function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: string): void { const originalRewrites = userNextConfig.rewrites; + // Allow overriding the tunnel destination for E2E tests via environment variable + const destinationOverride = process.env._SENTRY_TUNNEL_DESTINATION_OVERRIDE; + + // Make sure destinations are statically defined at build time + const destination = destinationOverride || 'https://o:orgid.ingest.sentry.io/api/:projectid/envelope/?hsts=0'; + const destinationWithRegion = + destinationOverride || 'https://o:orgid.ingest.:region.sentry.io/api/:projectid/envelope/?hsts=0'; // This function doesn't take any arguments at the time of writing but we future-proof // here in case Next.js ever decides to pass some @@ -419,7 +418,7 @@ function setUpTunnelRewriteRules( value: '(?\\d*)', }, ], - destination: destinationOverride || 'https://o:orgid.ingest.sentry.io/api/:projectid/envelope/?hsts=0', + destination, }; const tunnelRouteRewriteWithRegion = { @@ -443,7 +442,7 @@ function setUpTunnelRewriteRules( value: '(?[a-z]{2})', }, ], - destination: destinationOverride || 'https://o:orgid.ingest.:region.sentry.io/api/:projectid/envelope/?hsts=0', + destination: destinationWithRegion, }; // Order of these is important, they get applied first to last. From 5adae764c25e4809da48186512bcea2ea06995be Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 21 Nov 2025 16:02:36 +0200 Subject: [PATCH 16/16] chore: comments update --- .../nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts index b98711acb3a0..6f8b4eb96603 100644 --- a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts +++ b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts @@ -40,14 +40,11 @@ export function dropMiddlewareTunnelRequests(span: Span, attrs: SpanAttributes | * Checks if a span's HTTP target matches the tunnel route. */ function isTunnelRouteSpan(spanAttributes: Record): boolean { - // Don't use process.env here because it will have a different value in the build and runtime - // We want to use the one in build const tunnelPath = globalWithInjectedValues._sentryRewritesTunnelPath || process.env._sentryRewritesTunnelPath; if (!tunnelPath) { return false; } - // Check both http.target (older) and url.query (newer) attributes // eslint-disable-next-line deprecation/deprecation const httpTarget = spanAttributes[SEMATTRS_HTTP_TARGET];