Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const expectedTunnelPathMatcher =
? '/custom-monitor'
: tunnelRouteMode === 'object'
? '/object-monitor'
: /^\/[a-z0-9]{8}$/;
: /^\/[a-z0-9]{1,7}$/;

test.skip(tunnelRouteMode === 'off', 'Tunnel assertions only run in the tunnel-route variants');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@
"test:build:tunnel-static": "pnpm install && E2E_TEST_TUNNEL_ROUTE_MODE=static E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm build",
"test:build:tunnel-custom": "pnpm install && E2E_TEST_CUSTOM_TUNNEL_ROUTE=1 pnpm build",
"test:build:tunnel-object": "pnpm install && E2E_TEST_TUNNEL_ROUTE_MODE=object E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm build",
"test:build:tunnel-basepath": "pnpm install && E2E_TEST_TUNNEL_ROUTE_MODE=dynamic E2E_TEST_BASE_PATH=/app E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm build",
"test:build-latest": "pnpm add @tanstack/react-start@latest @tanstack/react-router@latest nitro@latest vite@latest @vitejs/plugin-react-swc@latest vite-tsconfig-paths@latest && pnpm install && pnpm build",
"test:assert:proxy": "pnpm test && TEST_ENV=development pnpm playwright test db-drivers",
"test:assert": "pnpm test:assert:proxy",
"test:assert:tunnel-generated": "E2E_TEST_TUNNEL_ROUTE_MODE=dynamic E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm test",
"test:assert:tunnel-static": "E2E_TEST_TUNNEL_ROUTE_MODE=static E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm test",
"test:assert:tunnel-custom": "E2E_TEST_CUSTOM_TUNNEL_ROUTE=1 pnpm test",
"test:assert:tunnel-object": "E2E_TEST_TUNNEL_ROUTE_MODE=object E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm test"
"test:assert:tunnel-object": "E2E_TEST_TUNNEL_ROUTE_MODE=object E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm test",
"test:assert:tunnel-basepath": "E2E_TEST_TUNNEL_ROUTE_MODE=dynamic E2E_TEST_BASE_PATH=/app E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm test tunnel",
"test:assert:tunnel-dev": "E2E_TEST_TUNNEL_ROUTE_MODE=dynamic E2E_TEST_DSN=http://public@localhost:3031/1337 TEST_ENV=development pnpm test tunnel",
"test:assert:tunnel-basepath-dev": "E2E_TEST_TUNNEL_ROUTE_MODE=dynamic E2E_TEST_BASE_PATH=/app E2E_TEST_DSN=http://public@localhost:3031/1337 TEST_ENV=development pnpm test tunnel"
},
"//": "Need to use ioredis 5.10.1 because that's the last version before they support tracing channels",
"dependencies": {
Expand Down Expand Up @@ -70,6 +74,21 @@
"label": "tanstackstart-react (tunnel-object)",
"build-command": "pnpm test:build:tunnel-object",
"assert-command": "pnpm test:assert:tunnel-object"
},
{
"label": "tanstackstart-react (tunnel-basepath)",
"build-command": "pnpm test:build:tunnel-basepath",
"assert-command": "pnpm test:assert:tunnel-basepath"
},
{
"label": "tanstackstart-react (tunnel-dev)",
"build-command": "pnpm install",
"assert-command": "pnpm test:assert:tunnel-dev"
},
{
"label": "tanstackstart-react (tunnel-basepath-dev)",
"build-command": "pnpm install",
"assert-command": "pnpm test:assert:tunnel-basepath-dev"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/t

const tunnelRouteMode =
process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? (process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1' ? 'custom' : 'off');
const basePath = process.env.E2E_TEST_BASE_PATH ?? '';
const expectedTunnelPathMatcher =
tunnelRouteMode === 'static'
? '/monitor'
: tunnelRouteMode === 'custom'
? '/custom-monitor'
: tunnelRouteMode === 'object'
? '/object-monitor'
: /^\/[a-z0-9]{8}$/;
: // The path `tunnelRoute: true` derives from the DSN.
new RegExp(`^${basePath}/[a-z0-9]{1,7}$`);

test.skip(tunnelRouteMode === 'off', 'Tunnel assertions only run in the tunnel-route variants');

Expand All @@ -19,7 +21,7 @@ test('Sends client-side errors through the configured tunnel route', async ({ pa
return errorEvent?.exception?.values?.[0]?.value === 'Sentry Client Test Error';
});

await page.goto('/');
await page.goto(`${basePath}/`);
const pageOrigin = new URL(page.url()).origin;

// Wait for React to hydrate (see __root.tsx) before clicking — the SSR HTML
Expand Down Expand Up @@ -54,20 +56,19 @@ test('Sends client-side errors through the configured tunnel route', async ({ pa
expect(managedTunnelUrl.pathname).toBe(expectedTunnelPathMatcher);
} else {
expect(managedTunnelUrl.pathname).toMatch(expectedTunnelPathMatcher);
expect(managedTunnelUrl.pathname).not.toBe('/monitor');
expect(managedTunnelUrl.pathname).not.toBe(`${basePath}/monitor`);
}

expect(errorEvent.exception?.values?.[0]?.value).toBe('Sentry Client Test Error');
expect(errorEvent.transaction).toBe('/');
expect(errorEvent.transaction).toBe(`${basePath}/`);
});

test('Rejects non-POST requests to the managed tunnel route instead of rendering the app', async ({ request }) => {
test('Only answers POSTs on the managed tunnel path and leaves other methods to the app', async ({ request }) => {
test.skip(tunnelRouteMode !== 'static', 'Requires a known managed tunnel path');

const response = await request.get('/monitor');

expect(response.status()).toBe(405);
expect(response.headers()['allow']).toBe('POST');
expect(response.status()).toBe(404);
});

function pathnameMatchesTunnelRoute(pathname: string): boolean {
Expand Down Expand Up @@ -101,7 +102,7 @@ test('Does not create a server transaction for the tunnel route', async ({ page
// transaction by the server SDK — tunnel traffic is plumbing, not application requests.
const tunnelServerEventPromise = waitForServerHttpSpan(pathnameMatchesTunnelRoute);

await page.goto('/');
await page.goto(`${basePath}/`);
const pageOrigin = new URL(page.url()).origin;

await page.locator('html[data-hydrated="true"]').waitFor();
Expand All @@ -127,7 +128,7 @@ test('Does not create a server transaction for the tunnel route', async ({ page
// this anchor. Racing the two lets us assert the absence of a tunnel transaction without idling on a timeout.
const anchorServerEventPromise = waitForServerHttpSpan(pathname => pathname.includes('/api/user/'));

await page.request.get('/api/user/456');
await page.request.get(`${basePath}/api/user/456`);

const winner = await Promise.race([
tunnelServerEventPromise.then(() => 'tunnel' as const),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ export default defineConfig({
},
plugins: [
tsConfigPaths(),
tanstackStart(),
// Start routes the whole app under the base path, so the tunnel has to be served under it too.
tanstackStart(
process.env.E2E_TEST_BASE_PATH ? { router: { basepath: process.env.E2E_TEST_BASE_PATH } } : undefined,
),
nitro(),
// react's vite plugin must come after start's vite plugin
viteReact(),
Expand Down
7 changes: 7 additions & 0 deletions packages/tanstackstart-react/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,10 @@ export function createSentryTunnelRoute(_options: CreateSentryTunnelRouteOptions
* can statically import it (the call is server-guarded and tree-shaken from the client bundle).
*/
export function registerSentryServerTunnelRoute(_path: string): void {}

/**
* No-op stub for client-side builds. The server entry the Vite plugin wraps only runs on the server.
*/
export function wrapServerEntryWithSentryTunnel<T>(entry: T, _options?: unknown): T {
return entry;
}
18 changes: 14 additions & 4 deletions packages/tanstackstart-react/src/client/tunnelRoute.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { consoleSandbox } from '@sentry/core';
import { applyTunnelDefaults, consoleSandbox } from '@sentry/core';
import type { BrowserOptions as ReactBrowserOptions } from '@sentry/react';

declare const __SENTRY_TANSTACKSTART_TUNNEL_ROUTE__: string | undefined;
declare const __SENTRY_TANSTACKSTART_TUNNEL_ROUTE__: string | true | undefined;
declare const __SENTRY_TANSTACKSTART_TUNNEL_BASE_PATH__: string | undefined;

let hasWarnedAboutManagedTunnelRouteOverride = false;

/**
* Applies the managed tunnel route from `sentryTanstackStart({ tunnelRoute: ... })` unless the user already
* configured an explicit runtime `tunnel` option in `Sentry.init()`.
* configured an explicit runtime `tunnel` option in `Sentry.init()`, then serves the tunnel under the base path
* Start routes on. A `basePath` the user set wins.
*/
export function applyTunnelRouteOption(options: ReactBrowserOptions): void {
applyManagedTunnelRoute(options);

const basePath =
typeof __SENTRY_TANSTACKSTART_TUNNEL_BASE_PATH__ !== 'undefined' ? __SENTRY_TANSTACKSTART_TUNNEL_BASE_PATH__ : '';
options.tunnel = applyTunnelDefaults(options.tunnel, basePath ? { basePath } : undefined);
}

function applyManagedTunnelRoute(options: ReactBrowserOptions): void {
const managedTunnelRoute =
typeof __SENTRY_TANSTACKSTART_TUNNEL_ROUTE__ !== 'undefined' ? __SENTRY_TANSTACKSTART_TUNNEL_ROUTE__ : undefined;

Expand All @@ -18,7 +28,7 @@ export function applyTunnelRouteOption(options: ReactBrowserOptions): void {
}

if (options.tunnel) {
if (!hasWarnedAboutManagedTunnelRouteOverride) {
if (options.tunnel !== managedTunnelRoute && !hasWarnedAboutManagedTunnelRouteOverride) {
hasWarnedAboutManagedTunnelRouteOverride = true;
consoleSandbox(() => {
// eslint-disable-next-line no-console
Expand Down
1 change: 1 addition & 0 deletions packages/tanstackstart-react/src/index.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ export declare const sentryGlobalRequestMiddleware: typeof serverSdk.sentryGloba
export declare const sentryGlobalFunctionMiddleware: typeof serverSdk.sentryGlobalFunctionMiddleware;
export declare const createSentryTunnelRoute: typeof serverSdk.createSentryTunnelRoute;
export declare const registerSentryServerTunnelRoute: typeof serverSdk.registerSentryServerTunnelRoute;
export declare const wrapServerEntryWithSentryTunnel: typeof serverSdk.wrapServerEntryWithSentryTunnel;
6 changes: 5 additions & 1 deletion packages/tanstackstart-react/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ export { init } from './sdk';
export { wrapFetchWithSentry } from './wrapFetchWithSentry';
export { wrapMiddlewaresWithSentry } from './middleware';
export { sentryGlobalRequestMiddleware, sentryGlobalFunctionMiddleware } from './globalMiddleware';
export { createSentryTunnelRoute, registerSentryServerTunnelRoute } from './tunnelRoute';
export {
createSentryTunnelRoute,
registerSentryServerTunnelRoute,
wrapServerEntryWithSentryTunnel,
} from './tunnelRoute';

/**
* A no-op stub of the replay integration for the server. Router setup code is shared between client and server,
Expand Down
48 changes: 47 additions & 1 deletion packages/tanstackstart-react/src/server/tunnelRoute.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { URL_PATH } from '@sentry/conventions/attributes';
import { dsnToString, escapeStringForRegex, getClient, handleTunnelRequest } from '@sentry/core';
import type { TunnelOptions } from '@sentry/core';
import {
dsnToString,
escapeStringForRegex,
getClient,
getTunnelPaths,
handleTunnelRequest,
handleTunnelRequestIfMatched,
} from '@sentry/core';
import type { ServerEntry } from './wrapFetchWithSentry';

const registeredTunnelRoutePaths = new Set<string>();

Expand Down Expand Up @@ -92,3 +101,40 @@ export function createSentryTunnelRoute(options: CreateSentryTunnelRouteOptions)
},
};
}

/**
* Answers managed tunnel requests before the server entry runs. The `sentryTanstackStart` Vite plugin applies this
* to the app's server entry automatically, with the base path Start routes on.
*/
export function wrapServerEntryWithSentryTunnel<T extends ServerEntry>(
entry: T,
options?: TunnelOptions,
basePath?: string,
): T {
// Requests still carry Start's base path here, so the tunnel is matched under it.
const getTunnelOptions = (): TunnelOptions | undefined => {
const tunnelOptions = options ?? getClient()?.getOptions()._managedTunnel;

return tunnelOptions && basePath ? { basePath, ...tunnelOptions } : tunnelOptions;
};

// When `init` ran before the server entry loads, drop the tunnel's spans from the first request on.
const startupOptions = getTunnelOptions();
getTunnelPaths(startupOptions).forEach(path =>
registerSentryServerTunnelRoute(`${startupOptions?.basePath?.replace(/\/$/, '') ?? ''}${path}`),
);

return {
...entry,
fetch(request: Request, opts?: unknown) {
const tunnelResponse = handleTunnelRequestIfMatched(request, getTunnelOptions());
if (tunnelResponse) {
registerSentryServerTunnelRoute(new URL(request.url).pathname);

return tunnelResponse;
}

return entry.fetch(request, opts);
},
};
}
11 changes: 8 additions & 3 deletions packages/tanstackstart-react/src/vite/sentryTanstackStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { makeAutoInstrumentMiddlewarePlugin } from './autoInstrumentMiddleware';
import { makeRoutePatternPlugin } from './routePatterns';
import { makeAddSentryVitePlugin, makeEnableSourceMapsVitePlugin } from './sourceMaps';
import type { TunnelRouteOptions } from './tunnelRoute';
import { makeTunnelRoutePlugin } from './tunnelRoute';
import { makeServerEntryTunnelPlugin, makeTunnelBasePathPlugin, makeTunnelRoutePlugin } from './tunnelRoute';

/**
* Build-time options for the Sentry TanStack Start SDK.
Expand Down Expand Up @@ -40,7 +40,7 @@ export interface SentryTanstackStartOptions extends BuildTimeOptionsBase {
* as the default `tunnel` option on the client.
*
* You can pass:
* - `true` to generate an opaque route path per dev session or production build.
* - `true` to serve the path derived from the DSN, the same one `Sentry.init({ tunnel: true })` uses.
* - `'/custom-path'` to use a fixed static route path.
* - `{ allowedDsns, path }` for full control. If `allowedDsns` is omitted or empty, the tunnel route derives the DSN
* from the active server Sentry client at runtime.
Expand Down Expand Up @@ -76,7 +76,12 @@ export interface SentryTanstackStartOptions extends BuildTimeOptionsBase {
* @returns An array of Vite plugins
*/
export function sentryTanstackStart(options: SentryTanstackStartOptions = {}): Plugin[] {
const plugins: Plugin[] = [makeRoutePatternPlugin()];
const tunnelBasePath = makeTunnelBasePathPlugin();
const plugins: Plugin[] = [
makeRoutePatternPlugin(),
tunnelBasePath.plugin,
makeServerEntryTunnelPlugin(options.tunnelRoute, tunnelBasePath.getBasePath),
];

if (options.tunnelRoute) {
plugins.push(makeTunnelRoutePlugin(options.tunnelRoute, options.debug));
Expand Down
Loading
Loading