Skip to content

Commit e1272c3

Browse files
committed
fix(sveltekit,astro,remix): Keep answering tunnel requests in the SDK's own request hook
Where no runtime hook answers tunnel requests, like on Vercel Edge, the request hook these SDKs already run is the only place left to answer them. It's one call, and where a runtime hook exists it answers first.
1 parent 0319645 commit e1272c3

6 files changed

Lines changed: 101 additions & 2 deletions

File tree

‎packages/astro/src/server/middleware.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ import {
2626
filterCollectedUrl,
2727
filterCollectedUrlQuery,
2828
} from '@sentry/core';
29-
import { flushIfServerless } from '@sentry/core/server';
29+
import {
30+
flushIfServerless,
31+
_INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest,
32+
} from '@sentry/core/server';
3033
import {
3134
captureException,
3235
continueTrace,
@@ -87,6 +90,11 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareHandler =
8790
return next();
8891
}
8992

93+
const tunnelResponse = handleServedTunnelRequest(ctx.request);
94+
if (tunnelResponse) {
95+
return tunnelResponse;
96+
}
97+
9098
const isDynamicPageRequest = checkIsDynamicPageRequest(ctx);
9199

92100
// For static (prerendered) routes, we only want to inject the parametrized route meta tags

‎packages/astro/test/server/middleware.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { HTTP_REQUEST_METHOD, SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH } f
22
import type { Client, Span } from '@sentry/core';
33

44
import * as SentryCore from '@sentry/core';
5+
import * as SentryCoreServer from '@sentry/core/server';
56
import * as SentryNode from '@sentry/node';
67
import type { APIContext } from 'astro';
78
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -33,6 +34,27 @@ const STATIC_REQUEST_CONTEXT = {
3334
url: new URL('https://myDomain.io/users/'),
3435
};
3536

37+
describe('sentryMiddleware with the managed tunnel', () => {
38+
it('answers tunnel requests without calling the rest of the middleware chain', async () => {
39+
const getClientSpy = vi.spyOn(SentryNode, 'getClient').mockReturnValue({} as Client);
40+
const tunnelResponse = new Response('forwarded');
41+
const matcherSpy = vi
42+
.spyOn(SentryCoreServer, '_INTERNAL_handleServedTunnelRequest')
43+
.mockReturnValue(Promise.resolve(tunnelResponse));
44+
const next = vi.fn();
45+
const request = new Request('https://myDomain.io/abc123', { method: 'POST' });
46+
47+
const response = await handleRequest()({ ...DYNAMIC_REQUEST_CONTEXT, request } as unknown as APIContext, next);
48+
49+
expect(response).toBe(tunnelResponse);
50+
expect(matcherSpy).toHaveBeenCalledWith(request);
51+
expect(next).not.toHaveBeenCalled();
52+
53+
getClientSpy.mockRestore();
54+
matcherSpy.mockRestore();
55+
});
56+
});
57+
3658
describe('sentryMiddleware', () => {
3759
const startSpanSpy = vi.spyOn(SentryNode, 'startSpan');
3860

‎packages/remix/src/v3/server/middleware.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
updateSpanName,
1111
winterCGRequestToRequestData,
1212
} from '@sentry/core';
13+
import { _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest } from '@sentry/core/server';
1314

1415
import { formatRouteTiming } from '../routeTiming';
1516
import type { MatcherLike, MiddlewareLike, NextFunctionLike, RequestContextLike } from '../types';
@@ -25,6 +26,11 @@ import { resolveRoutePattern } from './route';
2526
*/
2627
export function sentryRemixMiddleware(matcher: MatcherLike): MiddlewareLike {
2728
return async function sentryMiddleware(context: RequestContextLike, next: NextFunctionLike): Promise<Response> {
29+
const tunnelResponse = handleServedTunnelRequest(context.request);
30+
if (tunnelResponse) {
31+
return tunnelResponse;
32+
}
33+
2834
const isolationScope = getIsolationScope();
2935
isolationScope.setSDKProcessingMetadata({ normalizedRequest: winterCGRequestToRequestData(context.request) });
3036

‎packages/remix/test/v3/middleware.test.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ describe('sentryRemixMiddleware', () => {
4040
handleServedTunnelRequest.mockReturnValue(undefined);
4141
});
4242

43+
it('answers tunnel requests before route matching and the rest of the chain', async () => {
44+
const tunnelResponse = new Response('forwarded');
45+
handleServedTunnelRequest.mockReturnValue(Promise.resolve(tunnelResponse));
46+
const next = vi.fn();
47+
const context = contextFor('http://x/abc123');
48+
49+
const response = await sentryRemixMiddleware(matcherFor('/'))(context, next);
50+
51+
expect(response).toBe(tunnelResponse);
52+
expect(handleServedTunnelRequest).toHaveBeenCalledWith(context.request);
53+
expect(next).not.toHaveBeenCalled();
54+
});
55+
4356
it('propagates the trace to HTML responses through Server-Timing, ahead of the route', async () => {
4457
getTraceData.mockReturnValue({ 'sentry-trace': 'abc-def-1', baggage: 'sentry-trace_id=abc' });
4558
const middleware = sentryRemixMiddleware(matcherFor('/users/:id'));

‎packages/sveltekit/src/server-common/handle.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ import {
1919
withIsolationScope,
2020
filterCollectedUrl,
2121
} from '@sentry/core';
22-
import { flushIfServerless, _INTERNAL_setTunnelDefaults as setTunnelDefaults } from '@sentry/core/server';
22+
import {
23+
flushIfServerless,
24+
_INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest,
25+
_INTERNAL_setTunnelDefaults as setTunnelDefaults,
26+
} from '@sentry/core/server';
2327
import { getTunnelBasePath } from '../common/tunnel';
2428
import type { Handle, ResolveOptions } from './handleTypes';
2529
import { DEBUG_BUILD } from '../common/debug-build';
@@ -297,6 +301,11 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
297301
};
298302

299303
const sentryRequestHandler: Handle = input => {
304+
const tunnelResponse = handleServedTunnelRequest(input.event.request);
305+
if (tunnelResponse) {
306+
return tunnelResponse;
307+
}
308+
300309
const backwardsForwardsCompatibleEvent = input.event as typeof input.event & BackwardsForwardsCompatibleEvent;
301310

302311
// Escape hatch to suppress request isolation and trace continuation (see initCloudflareSentryHandle)

‎packages/sveltekit/test/server-common/handle.test.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getCapturedScopesOnSpan, getRootSpan, getSpanDescendants, spanIsSampled
44
import * as SentryCore from '@sentry/core';
55
import { NodeClient, serveTunnelIntegration, setCurrentClient } from '@sentry/node';
66
import { _INTERNAL_resetTunnelDefaults } from '@sentry/core/server';
7+
import { TUNNEL_BASE_PATH_GLOBAL } from '../../src/common/tunnel';
78
import type { Handle } from '../../src/server-common/handleTypes';
89
import { redirect } from '@sveltejs/kit';
910
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -145,6 +146,16 @@ describe('sentryHandle with span streaming', () => {
145146
describe('sentryHandle with the managed tunnel', () => {
146147
const DSN = 'https://public@o1.ingest.sentry.io/1337';
147148

149+
function makeTunnelRequest(path: string): Request {
150+
const body = SentryCore.serializeEnvelope(SentryCore.createEnvelope({ dsn: DSN }, [])) as string;
151+
152+
return new Request(`http://localhost:3000${path}`, {
153+
method: 'POST',
154+
body,
155+
headers: { 'content-length': String(body.length) },
156+
});
157+
}
158+
148159
beforeEach(() => {
149160
vi.stubGlobal(
150161
'fetch',
@@ -160,6 +171,36 @@ describe('sentryHandle with the managed tunnel', () => {
160171
_INTERNAL_resetTunnelDefaults();
161172
});
162173

174+
it('answers tunnel requests without resolving the route', async () => {
175+
const mockResolve = vi.fn();
176+
177+
const response = await sentryHandle()({
178+
event: mockEvent({ request: makeTunnelRequest(SentryCore.getTunnelPath(DSN)!), route: { id: null } }),
179+
resolve: mockResolve,
180+
});
181+
182+
expect(await response.text()).toBe('forwarded');
183+
expect(mockResolve).not.toHaveBeenCalled();
184+
});
185+
186+
it('serves the tunnel under the base path `sentrySvelteKit()` set before the handle was created', async () => {
187+
vi.stubGlobal(TUNNEL_BASE_PATH_GLOBAL, '/app');
188+
const mockResolve = vi.fn(() => Promise.resolve(new Response('app')));
189+
const handle = sentryHandle();
190+
191+
const underBasePath = await handle({
192+
event: mockEvent({ request: makeTunnelRequest(`/app${SentryCore.getTunnelPath(DSN)!}`), route: { id: null } }),
193+
resolve: mockResolve,
194+
});
195+
const atRoot = await handle({
196+
event: mockEvent({ request: makeTunnelRequest(SentryCore.getTunnelPath(DSN)!), route: { id: null } }),
197+
resolve: mockResolve,
198+
});
199+
200+
expect(await underBasePath.text()).toBe('forwarded');
201+
expect(await atRoot.text()).toBe('app');
202+
});
203+
163204
it('resolves every other request', async () => {
164205
const mockResolve = vi.fn(() => Promise.resolve(new Response('app')));
165206

0 commit comments

Comments
 (0)