Skip to content

Commit 5ee2da4

Browse files
authored
Merge pull request #20264 from getsentry/lazarnikolov/js-2140-tanstack-start-tunnel-adapter
Tunnel route helper + Dynamic tunnel route generator for TanStack Start React
2 parents 8f32e18 + af60d11 commit 5ee2da4

24 files changed

Lines changed: 880 additions & 8 deletions

dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,17 @@
99
"test": "playwright test",
1010
"clean": "npx rimraf node_modules pnpm-lock.yaml",
1111
"test:build": "pnpm install && pnpm build",
12+
"test:build:tunnel-generated": "pnpm install && E2E_TEST_TUNNEL_ROUTE_MODE=dynamic E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm build",
13+
"test:build:tunnel-static": "pnpm install && E2E_TEST_TUNNEL_ROUTE_MODE=static E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm build",
14+
"test:build:tunnel-custom": "pnpm install && E2E_TEST_CUSTOM_TUNNEL_ROUTE=1 pnpm build",
15+
"test:build:tunnel-object": "pnpm install && E2E_TEST_TUNNEL_ROUTE_MODE=object E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm build",
1216
"test:build-latest": "pnpm add @tanstack/react-start@latest @tanstack/react-router@latest && pnpm install && pnpm build",
13-
"test:assert": "pnpm test"
17+
"test:assert:proxy": "pnpm test",
18+
"test:assert": "pnpm test:assert:proxy",
19+
"test:assert:tunnel-generated": "E2E_TEST_TUNNEL_ROUTE_MODE=dynamic E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm test",
20+
"test:assert:tunnel-static": "E2E_TEST_TUNNEL_ROUTE_MODE=static E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm test",
21+
"test:assert:tunnel-custom": "E2E_TEST_CUSTOM_TUNNEL_ROUTE=1 pnpm test",
22+
"test:assert:tunnel-object": "E2E_TEST_TUNNEL_ROUTE_MODE=object E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm test"
1423
},
1524
"dependencies": {
1625
"@sentry/tanstackstart-react": "file:../../packed/sentry-tanstackstart-react-packed.tgz",
@@ -35,5 +44,29 @@
3544
},
3645
"volta": {
3746
"extends": "../../package.json"
47+
},
48+
"sentryTest": {
49+
"variants": [
50+
{
51+
"label": "tunnel-generated",
52+
"build-command": "pnpm test:build:tunnel-generated",
53+
"assert-command": "pnpm test:assert:tunnel-generated"
54+
},
55+
{
56+
"label": "tunnel-static",
57+
"build-command": "pnpm test:build:tunnel-static",
58+
"assert-command": "pnpm test:assert:tunnel-static"
59+
},
60+
{
61+
"label": "tunnel-custom",
62+
"build-command": "pnpm test:build:tunnel-custom",
63+
"assert-command": "pnpm test:assert:tunnel-custom"
64+
},
65+
{
66+
"label": "tunnel-object",
67+
"build-command": "pnpm test:build:tunnel-object",
68+
"assert-command": "pnpm test:assert:tunnel-object"
69+
}
70+
]
3871
}
3972
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
declare const __APP_DSN__: string;
2+
declare const __APP_TUNNEL__: string | undefined;

dev-packages/e2e-tests/test-applications/tanstackstart-react/src/router.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ export const getRouter = () => {
1111
if (!router.isServer) {
1212
Sentry.init({
1313
environment: 'qa', // dynamic sampling bias to keep transactions
14-
dsn: 'https://public@dsn.ingest.sentry.io/1337',
14+
dsn: __APP_DSN__,
1515
integrations: [Sentry.tanstackRouterBrowserTracingIntegration(router)],
1616
// We recommend adjusting this value in production, or using tracesSampler
1717
// for finer control
1818
tracesSampleRate: 1.0,
1919
release: 'e2e-test',
20-
tunnel: 'http://localhost:3031/', // proxy server
20+
tunnel: __APP_TUNNEL__,
2121
});
2222
}
2323

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as Sentry from '@sentry/tanstackstart-react';
2+
import { createFileRoute } from '@tanstack/react-router';
3+
4+
const USE_CUSTOM_TUNNEL_ROUTE = process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
5+
6+
const DEFAULT_DSN = 'https://public@dsn.ingest.sentry.io/1337';
7+
const TUNNEL_DSN = 'http://public@localhost:3031/1337';
8+
9+
// Example of a manually defined tunnel endpoint without relying on the
10+
// managed route injected by `sentryTanstackStart({ tunnelRoute: ... })`.
11+
// If you use a custom route like this one, set `tunnel: '/custom-monitor'` in the client SDK's
12+
// `Sentry.init()` call so browser events are sent to the same endpoint.
13+
export const Route = createFileRoute('/custom-monitor')({
14+
server: Sentry.createSentryTunnelRoute({
15+
allowedDsns: [USE_CUSTOM_TUNNEL_ROUTE ? TUNNEL_DSN : DEFAULT_DSN],
16+
}),
17+
});

dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
33

4+
const usesManagedTunnelRoute =
5+
(process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
6+
7+
test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant');
8+
49
test('Sends client-side error to Sentry with auto-instrumentation', async ({ page }) => {
510
const errorEventPromise = waitForError('tanstackstart-react', errorEvent => {
611
return errorEvent?.exception?.values?.[0]?.value === 'Sentry Client Test Error';

dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForTransaction } from '@sentry-internal/test-utils';
33

4+
const usesManagedTunnelRoute =
5+
(process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
6+
7+
test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant');
8+
49
test('Sends spans for multiple middlewares and verifies they are siblings under the same parent span', async ({
510
page,
611
}) => {

dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForTransaction } from '@sentry-internal/test-utils';
33

4+
const usesManagedTunnelRoute =
5+
(process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
6+
7+
test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant');
8+
49
test('Sends a server function transaction with auto-instrumentation', async ({ page }) => {
510
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
611
return (
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError } from '@sentry-internal/test-utils';
3+
4+
const tunnelRouteMode =
5+
process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? (process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1' ? 'custom' : 'off');
6+
const expectedTunnelPathMatcher =
7+
tunnelRouteMode === 'static'
8+
? '/monitor'
9+
: tunnelRouteMode === 'custom'
10+
? '/custom-monitor'
11+
: tunnelRouteMode === 'object'
12+
? '/object-monitor'
13+
: /^\/[a-z0-9]{8}$/;
14+
15+
test.skip(tunnelRouteMode === 'off', 'Tunnel assertions only run in the tunnel-route variants');
16+
17+
test('Sends client-side errors through the configured tunnel route', async ({ page }) => {
18+
const errorEventPromise = waitForError('tanstackstart-react', errorEvent => {
19+
return errorEvent?.exception?.values?.[0]?.value === 'Sentry Client Test Error';
20+
});
21+
22+
await page.goto('/');
23+
const pageOrigin = new URL(page.url()).origin;
24+
25+
await expect(page.locator('button').filter({ hasText: 'Break the client' })).toBeVisible();
26+
27+
const managedTunnelResponsePromise = page.waitForResponse(response => {
28+
const responseUrl = new URL(response.url());
29+
30+
return (
31+
responseUrl.origin === pageOrigin &&
32+
response.request().method() === 'POST' &&
33+
(typeof expectedTunnelPathMatcher === 'string'
34+
? responseUrl.pathname === expectedTunnelPathMatcher
35+
: expectedTunnelPathMatcher.test(responseUrl.pathname))
36+
);
37+
});
38+
39+
await page.locator('button').filter({ hasText: 'Break the client' }).click();
40+
41+
const managedTunnelResponse = await managedTunnelResponsePromise;
42+
const managedTunnelUrl = new URL(managedTunnelResponse.url());
43+
const errorEvent = await errorEventPromise;
44+
45+
expect(managedTunnelResponse.status()).toBe(200);
46+
expect(managedTunnelUrl.origin).toBe(pageOrigin);
47+
48+
if (typeof expectedTunnelPathMatcher === 'string') {
49+
expect(managedTunnelUrl.pathname).toBe(expectedTunnelPathMatcher);
50+
} else {
51+
expect(managedTunnelUrl.pathname).toMatch(expectedTunnelPathMatcher);
52+
expect(managedTunnelUrl.pathname).not.toBe('/monitor');
53+
}
54+
55+
expect(errorEvent.exception?.values?.[0]?.value).toBe('Sentry Client Test Error');
56+
expect(errorEvent.transaction).toBe('/');
57+
});

dev-packages/e2e-tests/test-applications/tanstackstart-react/vite.config.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,44 @@ import viteReact from '@vitejs/plugin-react-swc';
55
import { nitro } from 'nitro/vite';
66
import { sentryTanstackStart } from '@sentry/tanstackstart-react/vite';
77

8+
const tunnelRouteMode = process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off';
9+
const useManagedTunnelRoute = tunnelRouteMode !== 'off';
10+
const useCustomTunnelRoute = process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
11+
12+
const appDsn =
13+
useManagedTunnelRoute || useCustomTunnelRoute
14+
? 'http://public@localhost:3031/1337'
15+
: 'https://public@dsn.ingest.sentry.io/1337';
16+
17+
const appTunnel = useManagedTunnelRoute
18+
? undefined
19+
: useCustomTunnelRoute
20+
? '/custom-monitor'
21+
: 'http://localhost:3031/';
22+
23+
function resolveTunnelRouteOption() {
24+
switch (tunnelRouteMode) {
25+
case 'dynamic':
26+
return true;
27+
case 'static':
28+
return '/monitor';
29+
case 'object':
30+
return { path: '/object-monitor', allowedDsns: [appDsn] };
31+
default:
32+
return undefined;
33+
}
34+
}
35+
36+
const tunnelRoute = resolveTunnelRouteOption();
37+
838
export default defineConfig({
939
server: {
1040
port: 3000,
1141
},
42+
define: {
43+
__APP_DSN__: JSON.stringify(appDsn),
44+
__APP_TUNNEL__: appTunnel === undefined ? 'undefined' : JSON.stringify(appTunnel),
45+
},
1246
plugins: [
1347
tsConfigPaths(),
1448
tanstackStart(),
@@ -20,6 +54,7 @@ export default defineConfig({
2054
project: process.env.E2E_TEST_SENTRY_PROJECT,
2155
authToken: process.env.E2E_TEST_AUTH_TOKEN,
2256
debug: true,
57+
tunnelRoute,
2358
}),
2459
],
2560
});

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703
33
/* eslint-disable import/export */
44
import type { TanStackMiddlewareBase } from '../common/types';
5+
import type { CreateSentryTunnelRouteOptions } from '../server/tunnelRoute';
56

67
export * from '@sentry/react';
78

@@ -26,3 +27,19 @@ export const sentryGlobalRequestMiddleware: TanStackMiddlewareBase = { '~types':
2627
* The actual implementation is server-only, but this stub is needed to prevent rendering errors.
2728
*/
2829
export const sentryGlobalFunctionMiddleware: TanStackMiddlewareBase = { '~types': undefined, options: {} };
30+
31+
/**
32+
* No-op stub for client-side builds.
33+
* The actual implementation is server-only, but this stub is needed to prevent rendering errors.
34+
*/
35+
export function createSentryTunnelRoute(_options: CreateSentryTunnelRouteOptions): {
36+
handlers: {
37+
POST: () => Promise<Response>;
38+
};
39+
} {
40+
return {
41+
handlers: {
42+
POST: async () => new Response(null, { status: 500 }),
43+
},
44+
};
45+
}

0 commit comments

Comments
 (0)