Skip to content
Open
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 @@ -9,8 +9,15 @@
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install && pnpm build",
"test:build:tunnel-generated": "pnpm install && E2E_TEST_TUNNEL_ROUTE_MODE=dynamic pnpm build",
"test:build:tunnel-static": "pnpm install && E2E_TEST_TUNNEL_ROUTE_MODE=static pnpm build",
"test:build:tunnel-custom": "pnpm install && E2E_TEST_CUSTOM_TUNNEL_ROUTE=1 pnpm build",
"test:build-latest": "pnpm add @tanstack/react-start@latest @tanstack/react-router@latest && pnpm install && pnpm build",
"test:assert": "pnpm test"
"test:assert:proxy": "pnpm test",
"test:assert": "pnpm test:assert:proxy && E2E_TEST_TUNNEL_ROUTE_MODE=dynamic pnpm build && pnpm test:assert:tunnel-generated && E2E_TEST_TUNNEL_ROUTE_MODE=static pnpm build && pnpm test:assert:tunnel-static && E2E_TEST_CUSTOM_TUNNEL_ROUTE=1 pnpm build && pnpm test:assert:tunnel-custom",
"test:assert:tunnel-generated": "E2E_TEST_TUNNEL_ROUTE_MODE=dynamic pnpm test",
"test:assert:tunnel-static": "E2E_TEST_TUNNEL_ROUTE_MODE=static pnpm test",
"test:assert:tunnel-custom": "E2E_TEST_CUSTOM_TUNNEL_ROUTE=1 pnpm test"
},
"dependencies": {
"@sentry/tanstackstart-react": "latest || *",
Expand All @@ -35,5 +42,24 @@
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"variants": [
{
"label": "tunnel-generated",
"build-command": "pnpm test:build:tunnel-generated",
"assert-command": "pnpm test:assert:tunnel-generated"
},
{
"label": "tunnel-static",
"build-command": "pnpm test:build:tunnel-static",
"assert-command": "pnpm test:assert:tunnel-static"
},
{
"label": "tunnel-custom",
"build-command": "pnpm test:build:tunnel-custom",
"assert-command": "pnpm test:assert:tunnel-custom"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
declare const __APP_DSN__: string;
declare const __APP_TUNNEL__: string | undefined;
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ export const getRouter = () => {
if (!router.isServer) {
Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: 'https://public@dsn.ingest.sentry.io/1337',
dsn: __APP_DSN__,
integrations: [Sentry.tanstackRouterBrowserTracingIntegration(router)],
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
release: 'e2e-test',
tunnel: 'http://localhost:3031/', // proxy server
tunnel: __APP_TUNNEL__,
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as Sentry from "@sentry/tanstackstart-react";
import { createFileRoute } from "@tanstack/react-router";

const USE_CUSTOM_TUNNEL_ROUTE =
process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === "1";

const DEFAULT_DSN = "https://public@dsn.ingest.sentry.io/1337";
const TUNNEL_DSN = "http://public@localhost:3031/1337";

// Example of a manually defined tunnel endpoint without relying on the
// managed route injected by `sentryTanstackStart({ tunnelRoute: ... })`.
// If you use a custom route like this one, set `tunnel: '/custom-monitor'` in the client SDK's
// `Sentry.init()` call so browser events are sent to the same endpoint.
export const Route = createFileRoute("/custom-monitor")({
server: Sentry.createSentryTunnelRoute({
allowedDsns: [USE_CUSTOM_TUNNEL_ROUTE ? TUNNEL_DSN : DEFAULT_DSN],
}),
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';

const usesManagedTunnelRoute =
(process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';

test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant');

test('Sends client-side error to Sentry with auto-instrumentation', async ({ page }) => {
const errorEventPromise = waitForError('tanstackstart-react', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Sentry Client Test Error';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

const usesManagedTunnelRoute =
(process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';

test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant');

test('Sends spans for multiple middlewares and verifies they are siblings under the same parent span', async ({
page,
}) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

const usesManagedTunnelRoute =
(process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';

test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant');

test('Sends a server function transaction with auto-instrumentation', async ({ page }) => {
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';

const tunnelRouteMode =
process.env.E2E_TEST_TUNNEL_ROUTE_MODE ??
(process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1' ? 'custom' : 'off');
const expectedTunnelPathMatcher =
tunnelRouteMode === 'static'
? '/monitor'
: tunnelRouteMode === 'custom'
? '/custom-monitor'
: /^\/[a-z0-9]{8}$/;

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

test('Sends client-side errors through the configured tunnel route', async ({ page }) => {
const errorEventPromise = waitForError('tanstackstart-react', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Sentry Client Test Error';
});

await page.goto('/');
const pageOrigin = new URL(page.url()).origin;

await expect(page.locator('button').filter({ hasText: 'Break the client' })).toBeVisible();

const managedTunnelResponsePromise = page.waitForResponse(response => {
const responseUrl = new URL(response.url());

return (
responseUrl.origin === pageOrigin &&
response.request().method() === 'POST' &&
(typeof expectedTunnelPathMatcher === 'string'
? responseUrl.pathname === expectedTunnelPathMatcher
: expectedTunnelPathMatcher.test(responseUrl.pathname))
);
});
Comment thread
cursor[bot] marked this conversation as resolved.

await page.locator('button').filter({ hasText: 'Break the client' }).click();

const managedTunnelResponse = await managedTunnelResponsePromise;
const managedTunnelUrl = new URL(managedTunnelResponse.url());
const errorEvent = await errorEventPromise;

expect(managedTunnelResponse.status()).toBe(200);
expect(managedTunnelUrl.origin).toBe(pageOrigin);

if (typeof expectedTunnelPathMatcher === 'string') {
expect(managedTunnelUrl.pathname).toBe(expectedTunnelPathMatcher);
} else {
expect(managedTunnelUrl.pathname).toMatch(expectedTunnelPathMatcher);
expect(managedTunnelUrl.pathname).not.toBe('/monitor');
}
Comment thread
cursor[bot] marked this conversation as resolved.

expect(errorEvent.exception?.values?.[0]?.value).toBe('Sentry Client Test Error');
expect(errorEvent.transaction).toBe('/');
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,40 @@
import { defineConfig } from 'vite';
import tsConfigPaths from 'vite-tsconfig-paths';
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import viteReact from '@vitejs/plugin-react-swc';
import { nitro } from 'nitro/vite';
import { sentryTanstackStart } from '@sentry/tanstackstart-react/vite';
import { defineConfig } from "vite";
import tsConfigPaths from "vite-tsconfig-paths";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react-swc";
import { nitro } from "nitro/vite";
import { sentryTanstackStart } from "@sentry/tanstackstart-react/vite";

const tunnelRouteMode = process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? "off";
const useManagedTunnelRoute = tunnelRouteMode !== "off";
const useCustomTunnelRoute = process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === "1";

const appDsn = useManagedTunnelRoute || useCustomTunnelRoute
? "http://public@localhost:3031/1337"
: "https://public@dsn.ingest.sentry.io/1337";

const appTunnel = useManagedTunnelRoute
? undefined
: useCustomTunnelRoute
? "/custom-monitor"
: "http://localhost:3031/";

const tunnelRoute =
tunnelRouteMode === "dynamic"
? { allowedDsns: [appDsn], tunnel: true as const }
: tunnelRouteMode === "static"
? { allowedDsns: [appDsn], tunnel: "/monitor" }
: undefined;

export default defineConfig({
server: {
port: 3000,
},
define: {
__APP_DSN__: JSON.stringify(appDsn),
__APP_TUNNEL__:
appTunnel === undefined ? "undefined" : JSON.stringify(appTunnel),
},
plugins: [
tsConfigPaths(),
tanstackStart(),
Expand All @@ -20,6 +46,7 @@ export default defineConfig({
project: process.env.E2E_TEST_SENTRY_PROJECT,
authToken: process.env.E2E_TEST_AUTH_TOKEN,
debug: true,
tunnelRoute,
}),
],
});
17 changes: 17 additions & 0 deletions packages/tanstackstart-react/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703
/* eslint-disable import/export */
import type { TanStackMiddlewareBase } from '../common/types';
import type { CreateSentryTunnelRouteOptions } from '../server/tunnelRoute';

export * from '@sentry/react';

Expand All @@ -26,3 +27,19 @@ export const sentryGlobalRequestMiddleware: TanStackMiddlewareBase = { '~types':
* The actual implementation is server-only, but this stub is needed to prevent rendering errors.
*/
export const sentryGlobalFunctionMiddleware: TanStackMiddlewareBase = { '~types': undefined, options: {} };

/**
* No-op stub for client-side builds.
* The actual implementation is server-only, but this stub is needed to prevent rendering errors.
*/
export function createSentryTunnelRoute(_options: CreateSentryTunnelRouteOptions): {
handlers: {
POST: () => Promise<Response>;
};
} {
return {
handlers: {
POST: async () => new Response(null, { status: 500 }),
},
};
}
2 changes: 2 additions & 0 deletions packages/tanstackstart-react/src/client/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Client } from '@sentry/core';
import { applySdkMetadata } from '@sentry/core';
import type { BrowserOptions as ReactBrowserOptions } from '@sentry/react';
import { getDefaultIntegrations as getReactDefaultIntegrations, init as initReactSDK } from '@sentry/react';
import { applyTunnelRouteOption } from './tunnelRoute';

/**
* Initializes the TanStack Start React SDK
Expand All @@ -14,6 +15,7 @@ export function init(options: ReactBrowserOptions): Client | undefined {
...options,
};

applyTunnelRouteOption(sentryOptions);
applySdkMetadata(sentryOptions, 'tanstackstart-react', ['tanstackstart-react', 'react']);

return initReactSDK(sentryOptions);
Expand Down
37 changes: 37 additions & 0 deletions packages/tanstackstart-react/src/client/tunnelRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { consoleSandbox } from '@sentry/core';
import type { BrowserOptions as ReactBrowserOptions } from '@sentry/react';

declare const __SENTRY_TANSTACKSTART_TUNNEL_ROUTE__: 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()`.
*/
export function applyTunnelRouteOption(options: ReactBrowserOptions): void {
const managedTunnelRoute =
typeof __SENTRY_TANSTACKSTART_TUNNEL_ROUTE__ !== 'undefined'
? __SENTRY_TANSTACKSTART_TUNNEL_ROUTE__
: undefined;

if (!managedTunnelRoute) {
return;
}

if (options.tunnel) {
if (!hasWarnedAboutManagedTunnelRouteOverride) {
hasWarnedAboutManagedTunnelRouteOverride = true;
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.warn(
'[@sentry/tanstackstart-react] `Sentry.init({ tunnel: ... })` overrides the managed `sentryTanstackStart({ tunnelRoute: ... })` route. Remove the runtime `tunnel` option if you want the managed tunnel route to be used.',
);
});
}

return;
}

options.tunnel = managedTunnelRoute;
}
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 @@ -42,3 +42,4 @@ export declare const wrapMiddlewaresWithSentry: typeof serverSdk.wrapMiddlewares
export declare const tanstackRouterBrowserTracingIntegration: typeof clientSdk.tanstackRouterBrowserTracingIntegration;
export declare const sentryGlobalRequestMiddleware: typeof serverSdk.sentryGlobalRequestMiddleware;
export declare const sentryGlobalFunctionMiddleware: typeof serverSdk.sentryGlobalFunctionMiddleware;
export declare const createSentryTunnelRoute: typeof serverSdk.createSentryTunnelRoute;
1 change: 1 addition & 0 deletions packages/tanstackstart-react/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { init } from './sdk';
export { wrapFetchWithSentry } from './wrapFetchWithSentry';
export { wrapMiddlewaresWithSentry } from './middleware';
export { sentryGlobalRequestMiddleware, sentryGlobalFunctionMiddleware } from './globalMiddleware';
export { createSentryTunnelRoute } from './tunnelRoute';
Comment thread
cursor[bot] marked this conversation as resolved.

/**
* A no-op stub of the browser tracing integration for the server. Router setup code is shared between client and server,
Expand Down
43 changes: 43 additions & 0 deletions packages/tanstackstart-react/src/server/tunnelRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { handleTunnelRequest } from '@sentry/core';

export interface CreateSentryTunnelRouteOptions {
allowedDsns: string[];
}

type SentryTunnelRouteHandlerContext = {
request: Request;
};

type SentryTunnelRoute = {
handlers: {
POST: (context: SentryTunnelRouteHandlerContext) => Promise<Response>;
};
};

/**
* Creates a TanStack Start server route configuration for tunneling Sentry envelopes.
*
* @example
* ```ts
* import { createFileRoute } from '@tanstack/react-router';
* import * as Sentry from '@sentry/tanstackstart-react';
*
* export const Route = createFileRoute('/monitoring')({
* server: Sentry.createSentryTunnelRoute({
* allowedDsns: ['https://public@o0.ingest.sentry.io/0'],
* }),
* });
* ```
*/
export function createSentryTunnelRoute(options: CreateSentryTunnelRouteOptions): SentryTunnelRoute {
return {
handlers: {
POST: async ({ request }) => {
return handleTunnelRequest({
request,
allowedDsns: options.allowedDsns,
});
},
},
};
}
1 change: 1 addition & 0 deletions packages/tanstackstart-react/src/vite/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { sentryTanstackStart } from './sentryTanstackStart';
export type { SentryTanstackStartOptions } from './sentryTanstackStart';
export type { TunnelRouteOptions } from './tunnelRoute';
Loading
Loading