Skip to content

Commit ef7b5e9

Browse files
committed
Add a TanStack Start tunnel route helper
1 parent 134a66a commit ef7b5e9

4 files changed

Lines changed: 100 additions & 299 deletions

File tree

packages/tanstackstart-react/src/server/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export { init } from './sdk';
99
export { wrapFetchWithSentry } from './wrapFetchWithSentry';
1010
export { wrapMiddlewaresWithSentry } from './middleware';
1111
export { sentryGlobalRequestMiddleware, sentryGlobalFunctionMiddleware } from './globalMiddleware';
12+
export { createSentryTunnelRoute } from './tunnelRoute';
1213

1314
/**
1415
* A no-op stub of the browser tracing integration for the server. Router setup code is shared between client and server,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { handleTunnelRequest } from '@sentry/core';
2+
3+
export interface CreateSentryTunnelRouteOptions {
4+
allowedDsns: string[];
5+
}
6+
7+
type SentryTunnelRouteHandlerContext = {
8+
request: Request;
9+
};
10+
11+
type SentryTunnelRoute = {
12+
handlers: {
13+
POST: (context: SentryTunnelRouteHandlerContext) => Promise<Response>;
14+
};
15+
};
16+
17+
/**
18+
* Creates a TanStack Start server route configuration for tunneling Sentry envelopes.
19+
*
20+
* @example
21+
* ```ts
22+
* import { createFileRoute } from '@tanstack/react-router';
23+
* import * as Sentry from '@sentry/tanstackstart-react';
24+
*
25+
* export const Route = createFileRoute('/monitoring')({
26+
* server: Sentry.createSentryTunnelRoute({
27+
* allowedDsns: ['https://public@o0.ingest.sentry.io/0'],
28+
* }),
29+
* });
30+
* ```
31+
*/
32+
export function createSentryTunnelRoute(options: CreateSentryTunnelRouteOptions): SentryTunnelRoute {
33+
return {
34+
handlers: {
35+
POST: async ({ request }) => {
36+
return handleTunnelRequest({
37+
request,
38+
allowedDsns: options.allowedDsns,
39+
});
40+
},
41+
},
42+
};
43+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
3+
const handleTunnelRequestSpy = vi.fn();
4+
5+
vi.mock('@sentry/core', async importOriginal => {
6+
const original = await importOriginal();
7+
return {
8+
...original,
9+
handleTunnelRequest: (...args: unknown[]) => handleTunnelRequestSpy(...args),
10+
};
11+
});
12+
13+
const { createSentryTunnelRoute } = await import('../../src/server/tunnelRoute');
14+
15+
describe('createSentryTunnelRoute', () => {
16+
afterEach(() => {
17+
vi.clearAllMocks();
18+
});
19+
20+
it('returns a server route config with only a POST handler', () => {
21+
const route = createSentryTunnelRoute({
22+
allowedDsns: ['https://public@o0.ingest.sentry.io/0'],
23+
});
24+
25+
expect(Object.keys(route.handlers)).toEqual(['POST']);
26+
expect(route.handlers.POST).toBeTypeOf('function');
27+
});
28+
29+
it('forwards the request and allowed DSNs to handleTunnelRequest', async () => {
30+
const request = new Request('http://localhost:3000/monitoring', { method: 'POST', body: 'envelope' });
31+
const allowedDsns = ['https://public@o0.ingest.sentry.io/0'];
32+
const response = new Response('ok', { status: 200 });
33+
34+
handleTunnelRequestSpy.mockResolvedValueOnce(response);
35+
36+
const route = createSentryTunnelRoute({ allowedDsns });
37+
const result = await route.handlers.POST({ request });
38+
39+
expect(handleTunnelRequestSpy).toHaveBeenCalledTimes(1);
40+
const [options] = handleTunnelRequestSpy.mock.calls[0]!;
41+
expect(options).toEqual({
42+
request,
43+
allowedDsns,
44+
});
45+
expect(options.allowedDsns).toBe(allowedDsns);
46+
expect(result).toBe(response);
47+
});
48+
});

0 commit comments

Comments
 (0)