diff --git a/CHANGELOG.md b/CHANGELOG.md index f5caa55f6c4e..356bc6fa31c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ Server and client traces are now automatically connected, allowing you to see the full request lifecycle from server-side rendering through client-side hydration in a single trace. +- **feat(tanstackstart-react): Add server-side route parametrization ([#21147](https://github.com/getsentry/sentry-javascript/pull/21147))** + + Server transaction names are now parametrized automatically (e.g., `GET /users/123` becomes `GET /users/$userId`), improving transaction grouping in Sentry. + ## 10.54.0 ### Important Changes diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.user.$id.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.user.$id.ts new file mode 100644 index 000000000000..ce20e2f74ce1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.user.$id.ts @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/api/user/$id')({ + server: { + handlers: { + GET: async ({ params }) => { + return new Response(JSON.stringify({ id: params.id }), { + headers: { 'Content-Type': 'application/json' }, + }); + }, + }, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/param.$id.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/param.$id.tsx new file mode 100644 index 000000000000..43d32823168e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/param.$id.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/param/$id')({ + component: ParamPage, +}); + +function ParamPage() { + const { id } = Route.useParams(); + return ( +
+

Param: {id}

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/users.$userId.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/users.$userId.tsx new file mode 100644 index 000000000000..60f16f5cfece --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/users.$userId.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/users/$userId')({ + component: UserPage, +}); + +function UserPage() { + const { userId } = Route.useParams(); + return ( +
+

User: {userId}

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/users.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/users.tsx new file mode 100644 index 000000000000..97684c05df19 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/users.tsx @@ -0,0 +1,13 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/users')({ + component: UsersLayout, +}); + +function UsersLayout() { + return ( +
+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts new file mode 100644 index 000000000000..39058a45bf29 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts @@ -0,0 +1,82 @@ +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('should parametrize server and client transaction names for dynamic routes', async ({ page }) => { + const serverTxPromise = waitForTransaction('tanstackstart-react', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + typeof transactionEvent?.transaction === 'string' && + transactionEvent.transaction.includes('/param/') + ); + }); + + const clientTxPromise = waitForTransaction('tanstackstart-react', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'pageload' && + typeof transactionEvent?.transaction === 'string' && + transactionEvent.transaction.includes('/param/') + ); + }); + + await page.goto('/param/42'); + + const serverTx = await serverTxPromise; + const clientTx = await clientTxPromise; + + expect(serverTx.transaction).toBe('GET /param/$id'); + expect(serverTx.transaction_info?.source).toBe('route'); + + expect(clientTx.transaction).toBe('/param/$id'); + expect(clientTx.transaction_info?.source).toBe('route'); +}); + +test('should parametrize server and client transaction names for nested dynamic routes', async ({ page }) => { + const serverTxPromise = waitForTransaction('tanstackstart-react', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + typeof transactionEvent?.transaction === 'string' && + transactionEvent.transaction.includes('/users/') + ); + }); + + const clientTxPromise = waitForTransaction('tanstackstart-react', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'pageload' && + typeof transactionEvent?.transaction === 'string' && + transactionEvent.transaction.includes('/users/') + ); + }); + + await page.goto('/users/123'); + + const serverTx = await serverTxPromise; + const clientTx = await clientTxPromise; + + expect(serverTx.transaction).toBe('GET /users/$userId'); + expect(serverTx.transaction_info?.source).toBe('route'); + + expect(clientTx.transaction).toBe('/users/$userId'); + expect(clientTx.transaction_info?.source).toBe('route'); +}); + +test('should parametrize API route transaction names', async ({ baseURL }) => { + const serverTxPromise = waitForTransaction('tanstackstart-react', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + typeof transactionEvent?.transaction === 'string' && + transactionEvent.transaction.includes('/api/user/') + ); + }); + + await fetch(`${baseURL}/api/user/456`); + + const serverTx = await serverTxPromise; + + expect(serverTx.transaction).toBe('GET /api/user/$id'); + expect(serverTx.transaction_info?.source).toBe('route'); +}); diff --git a/packages/tanstackstart-react/src/server/routeParametrization.ts b/packages/tanstackstart-react/src/server/routeParametrization.ts new file mode 100644 index 000000000000..bd1d1d904770 --- /dev/null +++ b/packages/tanstackstart-react/src/server/routeParametrization.ts @@ -0,0 +1,66 @@ +import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; +import { + escapeStringForRegex, + getActiveSpan, + getCurrentScope, + getRootSpan, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + spanToJSON, + updateSpanName, +} from '@sentry/core'; + +function patternToRegex(pattern: string): RegExp { + const segments = pattern + .split('/') + .map(segment => { + if (segment.startsWith('$')) { + return '[^/]+'; + } + return escapeStringForRegex(segment); + }) + .join('/'); + return new RegExp(`^${segments}$`); +} + +/** + * Matches a URL pathname against a list of TanStack Start route patterns. + * Patterns use `$param` syntax for dynamic segments (e.g., `/users/$id`). + * + * Patterns are expected to be pre-sorted by specificity (more segments first, static before dynamic). + */ +export function matchUrlToRoutePattern(pathname: string, patterns: string[]): string | undefined { + const normalizedPathname = pathname.length > 1 ? pathname.replace(/\/$/, '') : pathname; + for (const pattern of patterns) { + if (patternToRegex(pattern).test(normalizedPathname)) { + return pattern; + } + } + return undefined; +} + +/** + * Updates the active root span with a parametrized route name. + */ +export function updateSpanWithRouteParametrization(method: string, pathname: string, patterns: string[]): void { + const matchedPattern = matchUrlToRoutePattern(pathname, patterns); + if (!matchedPattern) { + return; + } + + const activeSpan = getActiveSpan(); + if (!activeSpan) { + return; + } + + const rootSpan = getRootSpan(activeSpan); + const rootSpanData = spanToJSON(rootSpan).data; + if (rootSpanData?.[ATTR_HTTP_ROUTE]) { + return; + } + + const transactionName = `${method} ${matchedPattern}`; + updateSpanName(rootSpan, transactionName); + rootSpan.setAttribute(ATTR_HTTP_ROUTE, matchedPattern); + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + getCurrentScope().setTransactionName(transactionName); +} diff --git a/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts b/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts index 516604a94db1..73ea5604959e 100644 --- a/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts +++ b/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts @@ -5,8 +5,11 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, } from '@sentry/node'; +import { updateSpanWithRouteParametrization } from './routeParametrization'; import { extractServerFunctionSha256 } from './utils'; +declare const __SENTRY_ROUTE_PATTERNS__: string[] | undefined; + export type ServerEntry = { fetch: (request: Request, opts?: unknown) => Promise | Response; }; @@ -161,6 +164,10 @@ export function wrapFetchWithSentry(serverEntry: ServerEntry): ServerEntry { ); } + if (typeof __SENTRY_ROUTE_PATTERNS__ !== 'undefined') { + updateSpanWithRouteParametrization(method, url.pathname, __SENTRY_ROUTE_PATTERNS__); + } + return injectMetaTagsInResponse(await target.apply(thisArg, args)); } finally { await flushIfServerless(); diff --git a/packages/tanstackstart-react/src/vite/routePatterns.ts b/packages/tanstackstart-react/src/vite/routePatterns.ts new file mode 100644 index 000000000000..1c1b07fe0036 --- /dev/null +++ b/packages/tanstackstart-react/src/vite/routePatterns.ts @@ -0,0 +1,81 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import type { Plugin } from 'vite'; + +/** + * Extracts route patterns from TanStack Start's generated routeTree.gen.ts + * and replaces `__SENTRY_ROUTE_PATTERNS__` references with the extracted patterns. + * + * The route tree file is read during `transform` rather than `config` because + * TanStack Start generates it during the build. + */ +export function makeRoutePatternPlugin(): Plugin { + let resolvedRoot = ''; + + return { + name: 'sentry-tanstackstart-route-patterns', + enforce: 'post', + + configResolved(config) { + resolvedRoot = config.root || process.cwd(); + }, + + transform(code, id) { + // this is set in the `wrapFetchWithSentry` where the paths are getting replaced by their parametrized counterparts + // so this extraction should only happen once during the build (for the `wrapFetchWithSentry` file) + if (!code.includes('__SENTRY_ROUTE_PATTERNS__')) { + return null; + } + + // extract the patterns from the route tree file + const routeTreePath = path.resolve(resolvedRoot, 'src/routeTree.gen.ts'); + let patterns: string[] = []; + try { + if (fs.existsSync(routeTreePath)) { + patterns = extractRoutePatterns(fs.readFileSync(routeTreePath, 'utf-8')); + } + } catch { + // skip + } + + return { + code: code.replace(/__SENTRY_ROUTE_PATTERNS__/g, JSON.stringify(patterns)), + map: null, + }; + }, + }; +} + +/** + * Extracts full route path patterns from the content of routeTree.gen.ts. + * + * Parses the `fullPaths` type union which contains the resolved full paths + * (e.g., `fullPaths: '/' | '/page-a' | '/users/$userId'`). + * This is more reliable than `path:` properties which can be relative for nested routes. + */ +export function extractRoutePatterns(content: string): string[] { + const fullPathsMatch = content.match(/fullPaths:\s*([\s\S]*?)(?:\n\s*\w|\n\})/); + if (!fullPathsMatch) { + return []; + } + + const patterns: string[] = []; + const pathRegex = /['"]([^'"]+)['"]/g; + let match; + while ((match = pathRegex.exec(fullPathsMatch[1] || '')) !== null) { + if (match[1]) { + patterns.push(match[1]); + } + } + + return [...new Set(patterns)].sort((a, b) => { + const aSegments = a.split('/'); + const bSegments = b.split('/'); + if (bSegments.length !== aSegments.length) { + return bSegments.length - aSegments.length; + } + const aDynamic = aSegments.filter(s => s.startsWith('$')).length; + const bDynamic = bSegments.filter(s => s.startsWith('$')).length; + return aDynamic - bDynamic; + }); +} diff --git a/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts b/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts index 5682b67050ae..a440e791e242 100644 --- a/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts +++ b/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts @@ -1,6 +1,7 @@ import type { BuildTimeOptionsBase } from '@sentry/core'; import type { Plugin } from 'vite'; import { makeAutoInstrumentMiddlewarePlugin } from './autoInstrumentMiddleware'; +import { makeRoutePatternPlugin } from './routePatterns'; import { makeAddSentryVitePlugin, makeEnableSourceMapsVitePlugin } from './sourceMaps'; import type { TunnelRouteOptions } from './tunnelRoute'; import { makeTunnelRoutePlugin } from './tunnelRoute'; @@ -84,18 +85,18 @@ export interface SentryTanstackStartOptions extends BuildTimeOptionsBase { * @returns An array of Vite plugins */ export function sentryTanstackStart(options: SentryTanstackStartOptions = {}): Plugin[] { - const tunnelRoutePlugin = options.tunnelRoute ? makeTunnelRoutePlugin(options.tunnelRoute, options.debug) : undefined; + const plugins: Plugin[] = [makeRoutePatternPlugin()]; + + if (options.tunnelRoute) { + plugins.push(makeTunnelRoutePlugin(options.tunnelRoute, options.debug)); + } // only add build-time plugins in production builds if (process.env.NODE_ENV === 'development') { - return tunnelRoutePlugin ? [tunnelRoutePlugin] : []; + return plugins; } - const plugins: Plugin[] = [...makeAddSentryVitePlugin(options)]; - - if (tunnelRoutePlugin) { - plugins.push(tunnelRoutePlugin); - } + plugins.push(...makeAddSentryVitePlugin(options)); // middleware auto-instrumentation if (options.autoInstrumentMiddleware !== false) { diff --git a/packages/tanstackstart-react/test/server/routeParametrization.test.ts b/packages/tanstackstart-react/test/server/routeParametrization.test.ts new file mode 100644 index 000000000000..88fd2643dc9b --- /dev/null +++ b/packages/tanstackstart-react/test/server/routeParametrization.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { matchUrlToRoutePattern } from '../../src/server/routeParametrization'; + +describe('matchUrlToRoutePattern', () => { + // Pre-sorted by specificity: more segments first, static before dynamic + const patterns = ['/users/$userId/posts/$postId', '/api/health', '/page-a', '/page-b/$id', '/']; + + it('matches the root route', () => { + expect(matchUrlToRoutePattern('/', patterns)).toBe('/'); + }); + + it('matches a static route', () => { + expect(matchUrlToRoutePattern('/page-a', patterns)).toBe('/page-a'); + }); + + it('matches a single-param route', () => { + expect(matchUrlToRoutePattern('/page-b/42', patterns)).toBe('/page-b/$id'); + }); + + it('matches a multi-param route', () => { + expect(matchUrlToRoutePattern('/users/123/posts/456', patterns)).toBe('/users/$userId/posts/$postId'); + }); + + it('returns undefined for unmatched paths', () => { + expect(matchUrlToRoutePattern('/unknown', patterns)).toBeUndefined(); + }); + + it('returns undefined for partially matched paths', () => { + expect(matchUrlToRoutePattern('/page-b', patterns)).toBeUndefined(); + }); + + it('prefers static over dynamic matches when pre-sorted', () => { + const patternsWithOverlap = ['/page-b/special', '/page-b/$id']; + expect(matchUrlToRoutePattern('/page-b/special', patternsWithOverlap)).toBe('/page-b/special'); + }); + + it('prefers more specific routes when pre-sorted', () => { + const patternsNested = ['/users/$id/profile', '/users/$id']; + expect(matchUrlToRoutePattern('/users/123/profile', patternsNested)).toBe('/users/$id/profile'); + }); + + it('handles URLs with trailing slashes', () => { + expect(matchUrlToRoutePattern('/page-a/', patterns)).toBe('/page-a'); + expect(matchUrlToRoutePattern('/page-b/42/', patterns)).toBe('/page-b/$id'); + }); +}); diff --git a/packages/tanstackstart-react/test/vite/routePatterns.test.ts b/packages/tanstackstart-react/test/vite/routePatterns.test.ts new file mode 100644 index 000000000000..07b1c2803bff --- /dev/null +++ b/packages/tanstackstart-react/test/vite/routePatterns.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from 'vitest'; +import { extractRoutePatterns } from '../../src/vite/routePatterns'; + +describe('extractRoutePatterns', () => { + it('extracts full path patterns from single-line fullPaths union', () => { + const content = ` +export interface FileRouteTypes { + fullPaths: '/' | '/page-a' | '/page-b/$id' + fileRoutesByTo: FileRoutesByTo +} +`; + const patterns = extractRoutePatterns(content); + expect(patterns).toContain('/'); + expect(patterns).toContain('/page-a'); + expect(patterns).toContain('/page-b/$id'); + expect(patterns).toHaveLength(3); + }); + + it('extracts full path patterns from multi-line fullPaths union', () => { + const content = ` +export interface FileRouteTypes { + fullPaths: + | '/' + | '/page-a' + | '/page-b/$id' + | '/api/error' + fileRoutesByTo: FileRoutesByTo +} +`; + const patterns = extractRoutePatterns(content); + expect(patterns).toContain('/'); + expect(patterns).toContain('/page-a'); + expect(patterns).toContain('/page-b/$id'); + expect(patterns).toContain('/api/error'); + expect(patterns).toHaveLength(4); + }); + + it('returns empty array when fullPaths is not found', () => { + const patterns = extractRoutePatterns(''); + expect(patterns).toEqual([]); + }); + + it('extracts nested route full paths correctly', () => { + const content = ` +export interface FileRouteTypes { + fullPaths: + | '/' + | '/users' + | '/users/$userId' + | '/users/$userId/posts/$postId' + fileRoutesByTo: FileRoutesByTo +} +`; + const patterns = extractRoutePatterns(content); + expect(patterns).toContain('/users'); + expect(patterns).toContain('/users/$userId'); + expect(patterns).toContain('/users/$userId/posts/$postId'); + }); + + it('sorts patterns by specificity: more segments first, static before dynamic', () => { + const content = ` +export interface FileRouteTypes { + fullPaths: '/' | '/page-b/$id' | '/page-b/special' | '/users/$id/profile' | '/users/$id' + fileRoutesByTo: FileRoutesByTo +} +`; + const patterns = extractRoutePatterns(content); + expect(patterns).toEqual(['/users/$id/profile', '/page-b/special', '/page-b/$id', '/users/$id', '/']); + }); + + it('handles double-quoted paths (quoteStyle: "double")', () => { + const content = ` +export interface FileRouteTypes { + fullPaths: "/" | "/page-a" | "/page-b/$id" + fileRoutesByTo: FileRoutesByTo +} +`; + const patterns = extractRoutePatterns(content); + expect(patterns).toContain('/'); + expect(patterns).toContain('/page-a'); + expect(patterns).toContain('/page-b/$id'); + }); + + it('deduplicates patterns', () => { + const content = ` +export interface FileRouteTypes { + fullPaths: '/' | '/page-a' | '/page-a' + fileRoutesByTo: FileRoutesByTo +} +`; + const patterns = extractRoutePatterns(content); + expect(patterns.filter(p => p === '/page-a')).toHaveLength(1); + }); +}); diff --git a/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts b/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts index 516edadd0bb0..cba4508de1a1 100644 --- a/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts +++ b/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts @@ -35,6 +35,16 @@ const mockTunnelRoutePlugin: Plugin = { transform: vi.fn(), }; +const mockRoutePatternPlugin: Plugin = { + name: 'sentry-tanstackstart-route-patterns', + enforce: 'pre', + config: vi.fn(), +}; + +vi.mock('../../src/vite/routePatterns', () => ({ + makeRoutePatternPlugin: vi.fn(() => mockRoutePatternPlugin), +})); + vi.mock('../../src/vite/sourceMaps', () => ({ makeAddSentryVitePlugin: vi.fn(() => [mockSourceMapsConfigPlugin, mockSentryVitePlugin]), makeEnableSourceMapsVitePlugin: vi.fn(() => [mockEnableSourceMapsPlugin]), @@ -62,7 +72,12 @@ describe('sentryTanstackStart()', () => { it('returns source maps plugins in production mode', () => { const plugins = sentryTanstackStart({ autoInstrumentMiddleware: false }); - expect(plugins).toEqual([mockSourceMapsConfigPlugin, mockSentryVitePlugin, mockEnableSourceMapsPlugin]); + expect(plugins).toEqual([ + mockRoutePatternPlugin, + mockSourceMapsConfigPlugin, + mockSentryVitePlugin, + mockEnableSourceMapsPlugin, + ]); }); it('returns no plugins in development mode when tunnelRoute is not configured', () => { @@ -70,7 +85,7 @@ describe('sentryTanstackStart()', () => { const plugins = sentryTanstackStart({ autoInstrumentMiddleware: false }); - expect(plugins).toEqual([]); + expect(plugins).toEqual([mockRoutePatternPlugin]); }); it('returns only the tunnel route plugin in development mode when tunnelRoute is configured', () => { @@ -81,7 +96,7 @@ describe('sentryTanstackStart()', () => { tunnelRoute: { allowedDsns: ['https://public@o0.ingest.sentry.io/0'] }, }); - expect(plugins).toEqual([mockTunnelRoutePlugin]); + expect(plugins).toEqual([mockRoutePatternPlugin, mockTunnelRoutePlugin]); }); it('returns Sentry Vite plugins but not enable source maps plugin when sourcemaps.disable is true', () => { @@ -90,7 +105,7 @@ describe('sentryTanstackStart()', () => { sourcemaps: { disable: true }, }); - expect(plugins).toEqual([mockSourceMapsConfigPlugin, mockSentryVitePlugin]); + expect(plugins).toEqual([mockRoutePatternPlugin, mockSourceMapsConfigPlugin, mockSentryVitePlugin]); }); it('returns Sentry Vite plugins but not enable source maps plugin when sourcemaps.disable is "disable-upload"', () => { @@ -99,7 +114,7 @@ describe('sentryTanstackStart()', () => { sourcemaps: { disable: 'disable-upload' }, }); - expect(plugins).toEqual([mockSourceMapsConfigPlugin, mockSentryVitePlugin]); + expect(plugins).toEqual([mockRoutePatternPlugin, mockSourceMapsConfigPlugin, mockSentryVitePlugin]); }); it('returns Sentry Vite plugins and enable source maps plugin when sourcemaps.disable is false', () => { @@ -108,7 +123,12 @@ describe('sentryTanstackStart()', () => { sourcemaps: { disable: false }, }); - expect(plugins).toEqual([mockSourceMapsConfigPlugin, mockSentryVitePlugin, mockEnableSourceMapsPlugin]); + expect(plugins).toEqual([ + mockRoutePatternPlugin, + mockSourceMapsConfigPlugin, + mockSentryVitePlugin, + mockEnableSourceMapsPlugin, + ]); }); }); @@ -116,7 +136,12 @@ describe('sentryTanstackStart()', () => { it('includes middleware plugin by default', () => { const plugins = sentryTanstackStart({ sourcemaps: { disable: true } }); - expect(plugins).toEqual([mockSourceMapsConfigPlugin, mockSentryVitePlugin, mockMiddlewarePlugin]); + expect(plugins).toEqual([ + mockRoutePatternPlugin, + mockSourceMapsConfigPlugin, + mockSentryVitePlugin, + mockMiddlewarePlugin, + ]); }); it('includes middleware plugin when autoInstrumentMiddleware is true', () => { @@ -125,7 +150,12 @@ describe('sentryTanstackStart()', () => { sourcemaps: { disable: true }, }); - expect(plugins).toEqual([mockSourceMapsConfigPlugin, mockSentryVitePlugin, mockMiddlewarePlugin]); + expect(plugins).toEqual([ + mockRoutePatternPlugin, + mockSourceMapsConfigPlugin, + mockSentryVitePlugin, + mockMiddlewarePlugin, + ]); }); it('does not include middleware plugin when autoInstrumentMiddleware is false', () => { @@ -134,7 +164,7 @@ describe('sentryTanstackStart()', () => { sourcemaps: { disable: true }, }); - expect(plugins).toEqual([mockSourceMapsConfigPlugin, mockSentryVitePlugin]); + expect(plugins).toEqual([mockRoutePatternPlugin, mockSourceMapsConfigPlugin, mockSentryVitePlugin]); }); it('passes correct options to makeAutoInstrumentMiddlewarePlugin', () => { @@ -161,9 +191,10 @@ describe('sentryTanstackStart()', () => { }); expect(plugins).toEqual([ + mockRoutePatternPlugin, + mockTunnelRoutePlugin, mockSourceMapsConfigPlugin, mockSentryVitePlugin, - mockTunnelRoutePlugin, mockMiddlewarePlugin, ]); });