Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,8 @@ Because the integration owns error capture, `setupFastifyErrorHandler` no longer

**Tracing removed from generated templates:** Tracing was removed from the generated Pages Router API handler, Edge API handler, and Middleware wrapper templates. Route handlers and middleware are still instrumented automatically, so no action is required for most users.

**`tunnelRoute` requests now run through your middleware:** Webpack builds no longer skip your middleware for tunnel route requests. If your middleware blocks unauthenticated requests globally, exclude the tunnel route in its `matcher`, which requires a fixed string `tunnelRoute` instead of `true`.

**Unified `reactComponentAnnotation` option:** React component annotation is now configured through a single top-level `reactComponentAnnotation` option that applies to both webpack and Turbopack builds:

```js
Expand Down
25 changes: 0 additions & 25 deletions packages/nextjs/src/common/utils/tunnelPathnameMatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,3 @@
export function isPathnameUnderSentryTunnelRoute(pathname: string, tunnelPath: string): boolean {
return pathname === tunnelPath || pathname.startsWith(`${tunnelPath}/`);
}

/**
* Returns true only for requests the tunnel rewrite (see `setUpTunnelRewriteRules`) would serve.
*
* This decides whether the user's middleware is skipped, so it must never be broader than the rewrite:
* anything it matches that Next.js does not rewrite to Sentry reaches the app without middleware.
*/
export function isSentryTunnelRequest(request: Request, tunnelPath: string): boolean {
// The SDK transport only ever sends POST requests
if (request.method !== 'POST') {
return false;
}

const url = new URL(request.url);

if (url.pathname !== tunnelPath && url.pathname !== `${tunnelPath}/`) {
return false;
}

// Next.js evaluates `has` conditions against the last value of a repeated query param, so every value has to qualify
return ['o', 'p'].every(key => {
const values = url.searchParams.getAll(key);
return values.length > 0 && values.every(value => /^\d+$/.test(value));
});
}
21 changes: 0 additions & 21 deletions packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
withIsolationScope,
} from '@sentry/core';
import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd';
import { isSentryTunnelRequest } from '../common/utils/tunnelPathnameMatch';
import type { EdgeRouteHandler } from '../edge/types';

/**
Expand All @@ -27,26 +26,6 @@ export function wrapMiddlewareWithSentry<H extends EdgeRouteHandler>(
): (...params: Parameters<H>) => Promise<ReturnType<H>> {
return new Proxy(middleware, {
apply: async (wrappingTarget, thisArg, args: Parameters<H>) => {
const tunnelRoute =
'_sentryRewritesTunnelPath' in globalThis
? (globalThis as Record<string, unknown>)._sentryRewritesTunnelPath
: undefined;

// TODO: This can never work with Turbopack, need to remove it for consistency between builds.
if (tunnelRoute && typeof tunnelRoute === 'string') {
const req: unknown = args[0];
if (req instanceof Request && isSentryTunnelRequest(req, tunnelRoute)) {
// Create a simple response that mimics NextResponse.next() so we don't need to import Next.js internals here
// https://github.com/vercel/next.js/blob/c12c9c1f78ad384270902f0890dc4cd341408105/packages/next/src/server/web/spec-extension/response.ts#L146
return new Response(null, {
status: 200,
headers: {
'x-middleware-next': '1',
},
}) as ReturnType<H>;
}
}

// TODO: We still should add central isolation scope creation for when our build-time instrumentation does not work anymore with turbopack.
return withIsolationScope(isolationScope => {
const req: unknown = args[0];
Expand Down
4 changes: 4 additions & 0 deletions packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@ export type SentryBuildOptions = Omit<
* - Pass `true` to auto-generate a random, ad-blocker-resistant route for each build
* - Pass a string path (e.g., '/monitoring') to use a custom route
*
* Tunnel requests go through your middleware (`proxy.ts` / `middleware.ts`) like any other request. If your
* middleware redirects or blocks unauthenticated requests, exclude the tunnel route in its `matcher` so events
* can reach Sentry. Matchers have to be static, so use a fixed string route in that case rather than `true`.
Comment thread
chargome marked this conversation as resolved.
*
* NOTE: This feature only works with Next.js 11+
*/
tunnelRoute?: string | boolean;
Expand Down
89 changes: 2 additions & 87 deletions packages/nextjs/test/config/wrappers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,23 +104,6 @@ describe('wrapMiddlewareWithSentry', () => {
}
});

test('should skip processing and return NextResponse.next() for tunnel route requests', async () => {
// Set up tunnel route in global
(globalThis as any)._sentryRewritesTunnelPath = '/monitoring/tunnel';

const origFunction: EdgeRouteHandler = vi.fn(async () => ({ status: 200 }));
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);

// Create a mock Request that matches the tunnel route
const mockRequest = new Request('https://example.com/monitoring/tunnel?o=123&p=456', { method: 'POST' });

const result = await wrappedOriginal(mockRequest);

// Should skip calling the original function
expect(origFunction).not.toHaveBeenCalled();
expect(result).toBeDefined();
});

test('should process normal request and call original function', async () => {
const mockReturnValue = { status: 200 };
const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
Expand Down Expand Up @@ -160,82 +143,14 @@ describe('wrapMiddlewareWithSentry', () => {
expect(origFunction).toHaveBeenCalledWith(mockRequest);
});

test('should not process tunnel route when no tunnel path is set', async () => {
if ('_sentryRewritesTunnelPath' in globalThis) {
delete (globalThis as any)._sentryRewritesTunnelPath;
}

const mockReturnValue = { status: 200 };
const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);

const mockRequest = new Request('https://example.com/monitoring/tunnel/sentry?o=123');

const result = await wrappedOriginal(mockRequest);

// Should process normally since no tunnel path is configured
expect(origFunction).toHaveBeenCalledWith(mockRequest);
expect(result).toBe(mockReturnValue);
});

test('should process request when tunnel path is set but request does not match', async () => {
(globalThis as any)._sentryRewritesTunnelPath = '/monitoring/tunnel';

const mockReturnValue = { status: 200 };
const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);

const mockRequest = new Request('https://example.com/api/users', { method: 'GET' });

const result = await wrappedOriginal(mockRequest);

// Should process normally since request doesn't match tunnel path
expect(origFunction).toHaveBeenCalledWith(mockRequest);
expect(result).toBe(mockReturnValue);
});

test('should not treat paths as tunnel when they only share a prefix with tunnelRoute', async () => {
(globalThis as any)._sentryRewritesTunnelPath = '/api/t';

const mockReturnValue = { status: 200 };
const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);

const mockRequest = new Request('https://example.com/api/things', { method: 'GET' });

const result = await wrappedOriginal(mockRequest);

expect(origFunction).toHaveBeenCalledWith(mockRequest);
expect(result).toBe(mockReturnValue);
});

test('should skip processing for the tunnel route with a trailing slash', async () => {
(globalThis as any)._sentryRewritesTunnelPath = '/monitoring';

const origFunction: EdgeRouteHandler = vi.fn(async () => ({ status: 200 }));
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);

await wrappedOriginal(new Request('https://example.com/monitoring/?o=123&p=456&r=us', { method: 'POST' }));

expect(origFunction).not.toHaveBeenCalled();
});

test.each([
['a sub-path of the tunnel route', 'https://example.com/monitoring/anything/at/all?o=123&p=456', 'POST'],
['a tunnel request without query params', 'https://example.com/monitoring', 'POST'],
['a tunnel request without project id', 'https://example.com/monitoring?o=123', 'POST'],
['a tunnel request with non-numeric ids', 'https://example.com/monitoring?o=abc&p=456', 'POST'],
['a tunnel request with a repeated non-numeric org id', 'https://example.com/monitoring?o=123&o=abc&p=456', 'POST'],
['a tunnel request with a repeated empty project id', 'https://example.com/monitoring?o=123&p=456&p=', 'POST'],
['a non-POST tunnel request', 'https://example.com/monitoring?o=123&p=456', 'GET'],
])('should run the middleware for %s', async (_, url, method) => {
test('should run the middleware for requests to the tunnel route', async () => {
(globalThis as any)._sentryRewritesTunnelPath = '/monitoring';

const mockReturnValue = { status: 200 };
const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);

const mockRequest = new Request(url, { method });
const mockRequest = new Request('https://example.com/monitoring?o=123&p=456', { method: 'POST' });

const result = await wrappedOriginal(mockRequest);

Expand Down
Loading