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,11 @@
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install && pnpm build",
"test:build:tunnel": "pnpm install && E2E_TEST_USE_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_USE_TUNNEL_ROUTE=1 pnpm build && pnpm test:assert:tunnel",
"test:assert:tunnel": "E2E_TEST_USE_TUNNEL_ROUTE=1 pnpm test"
},
"dependencies": {
"@sentry/tanstackstart-react": "latest || *",
Expand All @@ -35,5 +38,14 @@
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"variants": [
{
"label": "tunnel",
"build-command": "pnpm test:build:tunnel",
"assert-command": "pnpm test:assert:tunnel"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
declare const __APP_DSN__: string;
declare const __APP_TUNNEL__: string;
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,13 @@
import * as Sentry from '@sentry/tanstackstart-react';
import { createFileRoute } from '@tanstack/react-router';

const USE_TUNNEL_ROUTE = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1';

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

export const Route = createFileRoute('/monitor')({
server: Sentry.createSentryTunnelRoute({
allowedDsns: [USE_TUNNEL_ROUTE ? TUNNEL_DSN : DEFAULT_DSN],
}),
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';

const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1';

test.skip(useTunnelRoute, '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,10 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1';

test.skip(useTunnelRoute, '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,10 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1';

test.skip(useTunnelRoute, '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,29 @@
import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';

const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1';

test.skip(!useTunnelRoute, 'Tunnel assertions only run in the tunnel variant');

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

await page.goto('/');

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

const monitorResponsePromise = page.waitForResponse(response => {
return response.url().endsWith('/monitor') && response.request().method() === 'POST';
});

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

const monitorResponse = await monitorResponsePromise;
const errorEvent = await errorEventPromise;

expect(monitorResponse.status()).toBe(200);
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
Expand Up @@ -5,10 +5,20 @@ import viteReact from '@vitejs/plugin-react-swc';
import { nitro } from 'nitro/vite';
import { sentryTanstackStart } from '@sentry/tanstackstart-react/vite';

const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1';

const appDsn = useTunnelRoute ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337';

const appTunnel = useTunnelRoute ? '/monitor' : 'http://localhost:3031/';

export default defineConfig({
server: {
port: 3000,
},
define: {
__APP_DSN__: JSON.stringify(appDsn),
__APP_TUNNEL__: JSON.stringify(appTunnel),
},
plugins: [
tsConfigPaths(),
tanstackStart(),
Expand Down
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';

/**
* 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,
});
},
},
};
}
48 changes: 48 additions & 0 deletions packages/tanstackstart-react/test/server/tunnelRoute.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { afterEach, describe, expect, it, vi } from 'vitest';

const handleTunnelRequestSpy = vi.fn();

vi.mock('@sentry/core', async importOriginal => {
const original = await importOriginal();
return {
...original,
handleTunnelRequest: (...args: unknown[]) => handleTunnelRequestSpy(...args),
};
});

const { createSentryTunnelRoute } = await import('../../src/server/tunnelRoute');

describe('createSentryTunnelRoute', () => {
afterEach(() => {
vi.clearAllMocks();
});

it('returns a server route config with only a POST handler', () => {
const route = createSentryTunnelRoute({
allowedDsns: ['https://public@o0.ingest.sentry.io/0'],
});

expect(Object.keys(route.handlers)).toEqual(['POST']);
expect(route.handlers.POST).toBeTypeOf('function');
});

it('forwards the request and allowed DSNs to handleTunnelRequest', async () => {
const request = new Request('http://localhost:3000/monitoring', { method: 'POST', body: 'envelope' });
const allowedDsns = ['https://public@o0.ingest.sentry.io/0'];
const response = new Response('ok', { status: 200 });

handleTunnelRequestSpy.mockResolvedValueOnce(response);

const route = createSentryTunnelRoute({ allowedDsns });
const result = await route.handlers.POST({ request });

expect(handleTunnelRequestSpy).toHaveBeenCalledTimes(1);
const [options] = handleTunnelRequestSpy.mock.calls[0]!;
expect(options).toEqual({
request,
allowedDsns,
});
expect(options.allowedDsns).toBe(allowedDsns);
expect(result).toBe(response);
});
});
Loading
Loading