diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 4e1e6bc2d9e3..c690b2665450 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -280,8 +280,8 @@ export type SentryBuildOptions = Omit< * export { POST } from '@sentry/nextjs/tunnel'; * ``` * - * This handler accepts the DSN of the server SDK, also a self-hosted one. For other DSNs, use - * `createSentryTunnelRoute` from the same import. + * This handler accepts the DSN of the server SDK, also a self-hosted one, and the DSNs `serveTunnelIntegration` + * allows. For other DSNs, use `createSentryTunnelRoute` from the same import. * * 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 diff --git a/packages/nextjs/src/tunnel/index.ts b/packages/nextjs/src/tunnel/index.ts index d86dd2d0a99d..c55e9cfbfa5c 100644 --- a/packages/nextjs/src/tunnel/index.ts +++ b/packages/nextjs/src/tunnel/index.ts @@ -1,9 +1,9 @@ -import { dsnToString, getClient, handleTunnelRequest, makeDsn, suppressTracing } from '@sentry/core'; +import { handleTunnelRequest } from '@sentry/core'; export interface CreateSentryTunnelRouteOptions { /** - * The DSNs that the route accepts. It rejects envelopes for other DSNs with a 403. - * Defaults to the DSN of the server SDK. + * DSNs whose envelopes the route forwards, in addition to the DSN of the server SDK. It rejects envelopes for other + * DSNs with a 403. Defaults to the DSNs `serveTunnelIntegration` allows. */ allowedDsns?: string[]; } @@ -27,24 +27,10 @@ type TunnelRouteHandler = (request: Request) => Promise; * ``` */ export function createSentryTunnelRoute(options: CreateSentryTunnelRouteOptions = {}): { POST: TunnelRouteHandler } { - const allowedDsnsFromOptions = options.allowedDsns?.length ? normalizeDsns(options.allowedDsns) : undefined; + const allowedDsns = options.allowedDsns?.length ? options.allowedDsns.map(dsn => dsn.trim()) : undefined; return { - POST: async request => { - // Read on each request, because the client can be set up after this module loads. - const allowedDsns = allowedDsnsFromOptions ?? getServerDsns(); - if (!allowedDsns.length) { - return new Response( - allowedDsnsFromOptions - ? 'Tunnel route has no valid DSN in allowedDsns.' - : 'Tunnel route requires Sentry server SDK initialized with a DSN, or pass allowedDsns explicitly.', - { status: 500 }, - ); - } - - // Like the SDK transports, so that the forward creates no spans, breadcrumbs or trace headers. - return suppressTracing(() => handleTunnelRequest(request, { allowedDsns })); - }, + POST: request => handleTunnelRequest(request, { allowedDsns }), }; } @@ -52,7 +38,8 @@ export function createSentryTunnelRoute(options: CreateSentryTunnelRouteOptions * Route handler for the `tunnelRoute` of `withSentryConfig`. It forwards Sentry envelopes to Sentry, but not the * request headers (such as cookies). * - * It accepts envelopes only for the DSN of the server SDK. For other DSNs, use `createSentryTunnelRoute`. + * It accepts envelopes for the DSN of the server SDK, and the DSNs `serveTunnelIntegration` allows. For other DSNs, + * use `createSentryTunnelRoute`. * * @example * ```ts @@ -61,16 +48,3 @@ export function createSentryTunnelRoute(options: CreateSentryTunnelRouteOptions * ``` */ export const POST: TunnelRouteHandler = createSentryTunnelRoute().POST; - -function getServerDsns(): string[] { - const dsn = getClient()?.getDsn(); - return dsn ? [dsnToString(dsn)] : []; -} - -// The same form as the DSN in the envelope header, because `handleTunnelRequest` compares exact strings. -function normalizeDsns(dsns: string[]): string[] { - return dsns.flatMap(dsn => { - const dsnComponents = makeDsn(dsn.trim()); - return dsnComponents ? [dsnToString(dsnComponents)] : []; - }); -} diff --git a/packages/nextjs/test/tunnel/index.test.ts b/packages/nextjs/test/tunnel/index.test.ts index 516543a2b5a9..a6fd021e3417 100644 --- a/packages/nextjs/test/tunnel/index.test.ts +++ b/packages/nextjs/test/tunnel/index.test.ts @@ -1,15 +1,9 @@ -import type * as SentryCore from '@sentry/core'; -import { createEnvelope, makeDsn, serializeEnvelope } from '@sentry/core'; +import type { Client } from '@sentry/core'; +import { createEnvelope, getCurrentScope, makeDsn, serializeEnvelope } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { serveTunnelIntegration } from '@sentry/core/server'; import { createSentryTunnelRoute, POST } from '../../src/tunnel'; -const { mockGetClient } = vi.hoisted(() => ({ mockGetClient: vi.fn() })); - -vi.mock('@sentry/core', async importOriginal => ({ - ...(await importOriginal()), - getClient: mockGetClient, -})); - const SERVER_DSN = 'https://public@o1.ingest.sentry.io/1'; const BROWSER_DSN = 'https://browser@o1.ingest.sentry.io/2'; @@ -33,7 +27,8 @@ function createEnvelopeRequest(dsn: string): { request: Request; body: Uint8Arra } function useClientDsn(dsn: string | undefined): void { - mockGetClient.mockReturnValue(dsn ? { getDsn: () => makeDsn(dsn) } : undefined); + const client = dsn ? ({ getDsn: () => makeDsn(dsn), getOptions: () => ({}) } as unknown as Client) : undefined; + getCurrentScope().setClient(client); } let fetchSpy: ReturnType; @@ -75,9 +70,6 @@ describe('POST', () => { const response = await POST(request); expect(response.status).toBe(500); - await expect(response.text()).resolves.toBe( - 'Tunnel route requires Sentry server SDK initialized with a DSN, or pass allowedDsns explicitly.', - ); expect(fetchSpy).not.toHaveBeenCalled(); }); @@ -91,6 +83,22 @@ describe('POST', () => { expect(fetchSpy).not.toHaveBeenCalled(); }); + it('accepts the DSNs `serveTunnelIntegration` allows', async () => { + const integration = serveTunnelIntegration({ allowedDsns: [BROWSER_DSN] }); + const options = {}; + const client = { + getDsn: () => makeDsn(SERVER_DSN), + getOptions: () => options, + getIntegrationByName: () => integration, + } as unknown as Client; + integration.setup?.(client); + getCurrentScope().setClient(client); + + const response = await POST(createEnvelopeRequest(BROWSER_DSN).request); + + expect(response.status).toBe(200); + }); + it('does not accept DSNs from environment variables', async () => { useClientDsn(SERVER_DSN); vi.stubEnv('SENTRY_DSN', BROWSER_DSN); @@ -104,16 +112,18 @@ describe('POST', () => { }); describe('createSentryTunnelRoute', () => { - it('accepts only the DSNs in `allowedDsns`', async () => { + it('accepts the DSNs in `allowedDsns` and the server DSN', async () => { useClientDsn(SERVER_DSN); const { POST: handler } = createSentryTunnelRoute({ allowedDsns: [BROWSER_DSN] }); - const allowedResponse = await handler(createEnvelopeRequest(BROWSER_DSN).request); - const rejectedResponse = await handler(createEnvelopeRequest(SERVER_DSN).request); + const browserResponse = await handler(createEnvelopeRequest(BROWSER_DSN).request); + const serverResponse = await handler(createEnvelopeRequest(SERVER_DSN).request); + const otherResponse = await handler(createEnvelopeRequest('https://other@o2.ingest.sentry.io/3').request); - expect(allowedResponse.status).toBe(200); - expect(rejectedResponse.status).toBe(403); - expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(browserResponse.status).toBe(200); + expect(serverResponse.status).toBe(200); + expect(otherResponse.status).toBe(403); + expect(fetchSpy).toHaveBeenCalledTimes(2); }); it.each([ @@ -138,15 +148,13 @@ describe('createSentryTunnelRoute', () => { expect(response.status).toBe(200); }); - it('responds with 500 when no allowed DSN is valid', async () => { + it('responds with 500 when no allowed DSN is valid and the server SDK has no DSN', async () => { vi.spyOn(console, 'error').mockImplementation(() => undefined); - useClientDsn(SERVER_DSN); const { POST: handler } = createSentryTunnelRoute({ allowedDsns: ['not-a-dsn'] }); const response = await handler(createEnvelopeRequest(SERVER_DSN).request); expect(response.status).toBe(500); - await expect(response.text()).resolves.toBe('Tunnel route has no valid DSN in allowedDsns.'); expect(fetchSpy).not.toHaveBeenCalled(); }); });