From b877f510adf55e8e84ab0d1eccda49a324869f92 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 27 Jan 2026 11:44:02 +0100 Subject: [PATCH 01/14] feat(tanstackstart-react): Auto-instrument request middleware --- .../src/vite/autoInstrumentMiddleware.ts | 78 ++++-- .../vite/autoInstrumentMiddleware.test.ts | 229 ++++++++++++++++++ 2 files changed, 283 insertions(+), 24 deletions(-) diff --git a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts index 6d898f233e1f..e04dfecdc01c 100644 --- a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts +++ b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts @@ -6,8 +6,9 @@ type AutoInstrumentMiddlewareOptions = { }; /** - * A Vite plugin that automatically instruments TanStack Start middlewares - * by wrapping `requestMiddleware` and `functionMiddleware` arrays in `createStart()`. + * A Vite plugin that automatically instruments TanStack Start middlewares: + * - `requestMiddleware` and `functionMiddleware` arrays in `createStart()` + * - `middleware` arrays in `createFileRoute()` route definitions */ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddlewareOptions = {}): Plugin { const { enabled = true, debug = false } = options; @@ -26,9 +27,11 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle return null; } - // Only wrap requestMiddleware and functionMiddleware in createStart() - // createStart() should always be in a file named start.ts - if (!id.includes('start') || !code.includes('createStart(')) { + // Detect file types that should be instrumented + const isStartFile = id.includes('start') && code.includes('createStart('); + const isRouteFile = code.includes('createFileRoute(') && /middleware\s*:\s*\[/.test(code); + + if (!isStartFile && !isRouteFile) { return null; } @@ -41,26 +44,53 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle let needsImport = false; const skippedMiddlewares: string[] = []; - transformed = transformed.replace( - /(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g, - (match: string, key: string, contents: string) => { - const objContents = arrayToObjectShorthand(contents); - if (objContents) { - needsImport = true; - if (debug) { - // eslint-disable-next-line no-console - console.log(`[Sentry] Auto-wrapping ${key} in ${id}`); + // Transform global middleware arrays in createStart() files + if (isStartFile) { + transformed = transformed.replace( + /(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g, + (match: string, key: string, contents: string) => { + const objContents = arrayToObjectShorthand(contents); + if (objContents) { + needsImport = true; + if (debug) { + // eslint-disable-next-line no-console + console.log(`[Sentry] Auto-wrapping ${key} in ${id}`); + } + return `${key}: wrapMiddlewaresWithSentry(${objContents})`; + } + // Track middlewares that couldn't be auto-wrapped + // Skip if we matched whitespace only + if (contents.trim()) { + skippedMiddlewares.push(key); } - return `${key}: wrapMiddlewaresWithSentry(${objContents})`; - } - // Track middlewares that couldn't be auto-wrapped - // Skip if we matched whitespace only - if (contents.trim()) { - skippedMiddlewares.push(key); - } - return match; - }, - ); + return match; + }, + ); + } + + // Transform route middleware arrays in createFileRoute() files + if (isRouteFile) { + transformed = transformed.replace( + /(\s+)(middleware)\s*:\s*\[([^\]]*)\]/g, + (match: string, whitespace: string, key: string, contents: string) => { + const objContents = arrayToObjectShorthand(contents); + if (objContents) { + needsImport = true; + if (debug) { + // eslint-disable-next-line no-console + console.log(`[Sentry] Auto-wrapping route ${key} in ${id}`); + } + return `${whitespace}${key}: wrapMiddlewaresWithSentry(${objContents})`; + } + // Track middlewares that couldn't be auto-wrapped + // Skip if we matched whitespace only + if (contents.trim()) { + skippedMiddlewares.push(`route ${key}`); + } + return match; + }, + ); + } // Warn about middlewares that couldn't be auto-wrapped if (skippedMiddlewares.length > 0) { diff --git a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts index 749b3e9822bd..4597805f6314 100644 --- a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts +++ b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts @@ -193,6 +193,235 @@ createStart(() => ({ }); }); +describe('route-level middleware auto-instrumentation', () => { + const routeFileWithMiddleware = ` +import { createFileRoute } from '@tanstack/react-router'; +import { loggingMiddleware } from '../middleware'; + +export const Route = createFileRoute('/api/test')({ + server: { + middleware: [loggingMiddleware], + handlers: { + GET: async () => ({ message: 'test' }), + }, + }, +}); +`; + + it('instruments route-level middleware arrays', () => { + const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; + const result = plugin.transform(routeFileWithMiddleware, '/app/routes/api.test.ts'); + + expect(result).not.toBeNull(); + expect(result!.code).toContain("import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react'"); + expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ loggingMiddleware })'); + }); + + it('instruments multiple middlewares in route file', () => { + const code = ` +import { createFileRoute } from '@tanstack/react-router'; +export const Route = createFileRoute('/foo')({ + server: { + middleware: [authMiddleware, loggingMiddleware], + handlers: { GET: () => ({}) }, + }, +}); +`; + const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; + const result = plugin.transform(code, '/app/routes/foo.ts'); + + expect(result).not.toBeNull(); + expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ authMiddleware, loggingMiddleware })'); + }); + + it('does not instrument files without createFileRoute', () => { + const code = ` +const middleware = [someMiddleware]; +export const foo = { middleware }; +`; + const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; + const result = plugin.transform(code, '/app/utils.ts'); + + expect(result).toBeNull(); + }); + + it('does not instrument route files without middleware arrays', () => { + const code = ` +import { createFileRoute } from '@tanstack/react-router'; +export const Route = createFileRoute('/client')({ + component: () => '
Client only
', +}); +`; + const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; + const result = plugin.transform(code, '/app/routes/client.tsx'); + + expect(result).toBeNull(); + }); + + it('does not instrument empty middleware arrays in route files', () => { + const code = ` +import { createFileRoute } from '@tanstack/react-router'; +export const Route = createFileRoute('/foo')({ + server: { + middleware: [], + handlers: { GET: () => ({}) }, + }, +}); +`; + const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; + const result = plugin.transform(code, '/app/routes/foo.ts'); + + expect(result).toBeNull(); + }); +}); + +describe('handler-specific middleware auto-instrumentation', () => { + it('instruments handler-level middleware arrays', () => { + const code = ` +import { createFileRoute } from '@tanstack/react-router'; +export const Route = createFileRoute('/foo')({ + server: { + handlers: ({ createHandlers }) => + createHandlers({ + GET: { + middleware: [loggingMiddleware], + handler: () => ({ data: 'test' }), + }, + }), + }, +}); +`; + const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; + const result = plugin.transform(code, '/app/routes/foo.ts'); + + expect(result).not.toBeNull(); + expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ loggingMiddleware })'); + }); + + it('instruments multiple handler-level middleware arrays in same file', () => { + const code = ` +import { createFileRoute } from '@tanstack/react-router'; +export const Route = createFileRoute('/foo')({ + server: { + handlers: ({ createHandlers }) => + createHandlers({ + GET: { + middleware: [readMiddleware], + handler: () => ({}), + }, + POST: { + middleware: [writeMiddleware, authMiddleware], + handler: () => ({}), + }, + }), + }, +}); +`; + const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; + const result = plugin.transform(code, '/app/routes/foo.ts'); + + expect(result).not.toBeNull(); + expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ readMiddleware })'); + expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ writeMiddleware, authMiddleware })'); + }); +}); + +describe('route middleware edge cases', () => { + it('does not wrap middleware containing function calls in route files', () => { + const code = ` +import { createFileRoute } from '@tanstack/react-router'; +export const Route = createFileRoute('/foo')({ + server: { + middleware: [createMiddleware()], + handlers: { GET: () => ({}) }, + }, +}); +`; + const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; + const result = plugin.transform(code, '/app/routes/foo.ts'); + + expect(result).toBeNull(); + }); + + it('warns about route middleware that cannot be auto-wrapped', () => { + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const code = ` +import { createFileRoute } from '@tanstack/react-router'; +export const Route = createFileRoute('/foo')({ + server: { + middleware: [getMiddleware()], + handlers: { GET: () => ({}) }, + }, +}); +`; + const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; + plugin.transform(code, '/app/routes/foo.ts'); + + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Could not auto-instrument route middleware')); + + consoleWarnSpy.mockRestore(); + }); + + it('handles route files with use server directive', () => { + const code = `'use server'; +import { createFileRoute } from '@tanstack/react-router'; +export const Route = createFileRoute('/foo')({ + server: { + middleware: [authMiddleware], + handlers: { GET: () => ({}) }, + }, +}); +`; + const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; + const result = plugin.transform(code, '/app/routes/foo.ts'); + + expect(result).not.toBeNull(); + expect(result!.code).toMatch(/^'use server';\s*\nimport \{ wrapMiddlewaresWithSentry \}/); + }); + + it('does not instrument route files that already use wrapMiddlewaresWithSentry', () => { + const code = ` +import { createFileRoute } from '@tanstack/react-router'; +import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react'; +export const Route = createFileRoute('/foo')({ + server: { + middleware: wrapMiddlewaresWithSentry({ authMiddleware }), + handlers: { GET: () => ({}) }, + }, +}); +`; + const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; + const result = plugin.transform(code, '/app/routes/foo.ts'); + + expect(result).toBeNull(); + }); + + it('handles both route-level and handler-level middleware in same file', () => { + const code = ` +import { createFileRoute } from '@tanstack/react-router'; +export const Route = createFileRoute('/foo')({ + server: { + middleware: [routeMiddleware], + handlers: ({ createHandlers }) => + createHandlers({ + GET: { + middleware: [getMiddleware], + handler: () => ({}), + }, + }), + }, +}); +`; + const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; + const result = plugin.transform(code, '/app/routes/foo.ts'); + + expect(result).not.toBeNull(); + expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ routeMiddleware })'); + expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ getMiddleware })'); + }); +}); + describe('arrayToObjectShorthand', () => { it('converts single identifier', () => { expect(arrayToObjectShorthand('foo')).toBe('{ foo }'); From 40a7ba42d403e4f96754dacf5e4d89763516d250 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 27 Jan 2026 12:02:31 +0100 Subject: [PATCH 02/14] refactor --- .../src/vite/autoInstrumentMiddleware.ts | 118 ++++-- .../vite/autoInstrumentMiddleware.test.ts | 368 +++++++----------- 2 files changed, 225 insertions(+), 261 deletions(-) diff --git a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts index e04dfecdc01c..f53119f350fc 100644 --- a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts +++ b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts @@ -5,6 +5,74 @@ type AutoInstrumentMiddlewareOptions = { debug?: boolean; }; +type WrapResult = { + code: string; + didWrap: boolean; + skipped: string[]; +}; + +/** + * Wraps global middleware arrays (requestMiddleware, functionMiddleware) in createStart() files. + */ +export function wrapGlobalMiddleware(code: string, id: string, debug: boolean): WrapResult { + const skipped: string[] = []; + let didWrap = false; + + const transformed = code.replace( + /(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g, + (match: string, key: string, contents: string) => { + const objContents = arrayToObjectShorthand(contents); + if (objContents) { + didWrap = true; + if (debug) { + // eslint-disable-next-line no-console + console.log(`[Sentry] Auto-wrapping ${key} in ${id}`); + } + return `${key}: wrapMiddlewaresWithSentry(${objContents})`; + } + // Track middlewares that couldn't be auto-wrapped + // Skip if we matched whitespace only + if (contents.trim()) { + skipped.push(key); + } + return match; + }, + ); + + return { code: transformed, didWrap, skipped }; +} + +/** + * Wraps route middleware arrays in createFileRoute() files. + */ +export function wrapRouteMiddleware(code: string, id: string, debug: boolean): WrapResult { + const skipped: string[] = []; + let didWrap = false; + + const transformed = code.replace( + /(\s+)(middleware)\s*:\s*\[([^\]]*)\]/g, + (match: string, whitespace: string, key: string, contents: string) => { + const objContents = arrayToObjectShorthand(contents); + if (objContents) { + didWrap = true; + if (debug) { + // eslint-disable-next-line no-console + console.log(`[Sentry] Auto-wrapping route ${key} in ${id}`); + } + return `${whitespace}${key}: wrapMiddlewaresWithSentry(${objContents})`; + } + // Track middlewares that couldn't be auto-wrapped + // Skip if we matched whitespace only + if (contents.trim()) { + skipped.push(`route ${key}`); + } + return match; + }, + ); + + return { code: transformed, didWrap, skipped }; +} + /** * A Vite plugin that automatically instruments TanStack Start middlewares: * - `requestMiddleware` and `functionMiddleware` arrays in `createStart()` @@ -44,52 +112,18 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle let needsImport = false; const skippedMiddlewares: string[] = []; - // Transform global middleware arrays in createStart() files if (isStartFile) { - transformed = transformed.replace( - /(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g, - (match: string, key: string, contents: string) => { - const objContents = arrayToObjectShorthand(contents); - if (objContents) { - needsImport = true; - if (debug) { - // eslint-disable-next-line no-console - console.log(`[Sentry] Auto-wrapping ${key} in ${id}`); - } - return `${key}: wrapMiddlewaresWithSentry(${objContents})`; - } - // Track middlewares that couldn't be auto-wrapped - // Skip if we matched whitespace only - if (contents.trim()) { - skippedMiddlewares.push(key); - } - return match; - }, - ); + const result = wrapGlobalMiddleware(transformed, id, debug); + transformed = result.code; + needsImport = needsImport || result.didWrap; + skippedMiddlewares.push(...result.skipped); } - // Transform route middleware arrays in createFileRoute() files if (isRouteFile) { - transformed = transformed.replace( - /(\s+)(middleware)\s*:\s*\[([^\]]*)\]/g, - (match: string, whitespace: string, key: string, contents: string) => { - const objContents = arrayToObjectShorthand(contents); - if (objContents) { - needsImport = true; - if (debug) { - // eslint-disable-next-line no-console - console.log(`[Sentry] Auto-wrapping route ${key} in ${id}`); - } - return `${whitespace}${key}: wrapMiddlewaresWithSentry(${objContents})`; - } - // Track middlewares that couldn't be auto-wrapped - // Skip if we matched whitespace only - if (contents.trim()) { - skippedMiddlewares.push(`route ${key}`); - } - return match; - }, - ); + const result = wrapRouteMiddleware(transformed, id, debug); + transformed = result.code; + needsImport = needsImport || result.didWrap; + skippedMiddlewares.push(...result.skipped); } // Warn about middlewares that couldn't be auto-wrapped diff --git a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts index 4597805f6314..17db2a96a849 100644 --- a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts +++ b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts @@ -1,6 +1,11 @@ import type { Plugin } from 'vite'; import { describe, expect, it, vi } from 'vitest'; -import { arrayToObjectShorthand, makeAutoInstrumentMiddlewarePlugin } from '../../src/vite/autoInstrumentMiddleware'; +import { + arrayToObjectShorthand, + makeAutoInstrumentMiddlewarePlugin, + wrapGlobalMiddleware, + wrapRouteMiddleware, +} from '../../src/vite/autoInstrumentMiddleware'; type PluginWithTransform = Plugin & { transform: (code: string, id: string) => { code: string; map: null } | null; @@ -9,31 +14,18 @@ type PluginWithTransform = Plugin & { describe('makeAutoInstrumentMiddlewarePlugin', () => { const createStartFile = ` import { createStart } from '@tanstack/react-start'; -import { authMiddleware, loggingMiddleware } from './middleware'; - -export const startInstance = createStart(() => ({ - requestMiddleware: [authMiddleware], - functionMiddleware: [loggingMiddleware], -})); +createStart(() => ({ requestMiddleware: [authMiddleware] })); `; - it('instruments a file with createStart and middleware arrays', () => { - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const result = plugin.transform(createStartFile, '/app/start.ts'); - - expect(result).not.toBeNull(); - expect(result!.code).toContain("import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react'"); - expect(result!.code).toContain('requestMiddleware: wrapMiddlewaresWithSentry({ authMiddleware })'); - expect(result!.code).toContain('functionMiddleware: wrapMiddlewaresWithSentry({ loggingMiddleware })'); - }); - - it('does not instrument files without createStart', () => { - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const code = "export const foo = 'bar';"; - const result = plugin.transform(code, '/app/other.ts'); - - expect(result).toBeNull(); - }); + const routeFile = ` +import { createFileRoute } from '@tanstack/react-router'; +export const Route = createFileRoute('/foo')({ + server: { + middleware: [authMiddleware], + handlers: { GET: () => ({}) }, + }, +}); +`; it('does not instrument non-TS/JS files', () => { const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; @@ -49,46 +41,10 @@ export const startInstance = createStart(() => ({ expect(result).toBeNull(); }); - it('wraps single middleware entry correctly', () => { + it('does not instrument files without createStart or createFileRoute', () => { const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const code = ` -import { createStart } from '@tanstack/react-start'; -createStart(() => ({ requestMiddleware: [singleMiddleware] })); -`; - const result = plugin.transform(code, '/app/start.ts'); - - expect(result!.code).toContain('requestMiddleware: wrapMiddlewaresWithSentry({ singleMiddleware })'); - }); - - it('wraps multiple middleware entries correctly', () => { - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const code = ` -import { createStart } from '@tanstack/react-start'; -createStart(() => ({ requestMiddleware: [a, b, c] })); -`; - const result = plugin.transform(code, '/app/start.ts'); - - expect(result!.code).toContain('requestMiddleware: wrapMiddlewaresWithSentry({ a, b, c })'); - }); - - it('does not wrap empty middleware arrays', () => { - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const code = ` -import { createStart } from '@tanstack/react-start'; -createStart(() => ({ requestMiddleware: [] })); -`; - const result = plugin.transform(code, '/app/start.ts'); - - expect(result).toBeNull(); - }); - - it('does not wrap if middleware contains function calls', () => { - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const code = ` -import { createStart } from '@tanstack/react-start'; -createStart(() => ({ requestMiddleware: [getMiddleware()] })); -`; - const result = plugin.transform(code, '/app/start.ts'); + const code = "export const foo = 'bar';"; + const result = plugin.transform(code, '/app/other.ts'); expect(result).toBeNull(); }); @@ -129,35 +85,27 @@ createStart(() => ({ requestMiddleware: [authMiddleware] })); expect(result!.code).toMatch(/^"use client";\s*\nimport \{ wrapMiddlewaresWithSentry \}/); }); - it('handles trailing commas in middleware arrays', () => { + it('adds import statement when wrapping middlewares', () => { const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const code = ` -import { createStart } from '@tanstack/react-start'; -createStart(() => ({ requestMiddleware: [authMiddleware,] })); -`; - const result = plugin.transform(code, '/app/start.ts'); + const result = plugin.transform(createStartFile, '/app/start.ts'); expect(result).not.toBeNull(); - expect(result!.code).toContain('requestMiddleware: wrapMiddlewaresWithSentry({ authMiddleware })'); + expect(result!.code).toContain("import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react'"); }); - it('wraps valid array and skips invalid array in same file', () => { + it('instruments both start files and route files', () => { const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const code = ` -import { createStart } from '@tanstack/react-start'; -createStart(() => ({ - requestMiddleware: [authMiddleware], - functionMiddleware: [getMiddleware()] -})); -`; - const result = plugin.transform(code, '/app/start.ts'); - expect(result).not.toBeNull(); - expect(result!.code).toContain('requestMiddleware: wrapMiddlewaresWithSentry({ authMiddleware })'); - expect(result!.code).toContain('functionMiddleware: [getMiddleware()]'); + const startResult = plugin.transform(createStartFile, '/app/start.ts'); + expect(startResult).not.toBeNull(); + expect(startResult!.code).toContain('wrapMiddlewaresWithSentry'); + + const routeResult = plugin.transform(routeFile, '/app/routes/foo.ts'); + expect(routeResult).not.toBeNull(); + expect(routeResult!.code).toContain('wrapMiddlewaresWithSentry'); }); - it('warns when middleware contains expressions that cannot be auto-wrapped', () => { + it('warns about middlewares that cannot be auto-wrapped', () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; @@ -171,114 +119,125 @@ createStart(() => ({ requestMiddleware: [getMiddleware()] })); consoleWarnSpy.mockRestore(); }); +}); - it('warns about skipped middlewares even when others are successfully wrapped', () => { - const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; +describe('wrapGlobalMiddleware', () => { + it('wraps requestMiddleware and functionMiddleware arrays', () => { const code = ` -import { createStart } from '@tanstack/react-start'; createStart(() => ({ requestMiddleware: [authMiddleware], - functionMiddleware: [getMiddleware()] + functionMiddleware: [loggingMiddleware], })); `; - plugin.transform(code, '/app/start.ts'); + const result = wrapGlobalMiddleware(code, '/app/start.ts', false); + + expect(result.didWrap).toBe(true); + expect(result.code).toContain('requestMiddleware: wrapMiddlewaresWithSentry({ authMiddleware })'); + expect(result.code).toContain('functionMiddleware: wrapMiddlewaresWithSentry({ loggingMiddleware })'); + expect(result.skipped).toHaveLength(0); + }); - expect(consoleWarnSpy).toHaveBeenCalledWith( - expect.stringContaining('Could not auto-instrument functionMiddleware'), - ); + it('wraps single middleware entry correctly', () => { + const code = 'createStart(() => ({ requestMiddleware: [singleMiddleware] }));'; + const result = wrapGlobalMiddleware(code, '/app/start.ts', false); - consoleWarnSpy.mockRestore(); + expect(result.didWrap).toBe(true); + expect(result.code).toContain('requestMiddleware: wrapMiddlewaresWithSentry({ singleMiddleware })'); }); -}); -describe('route-level middleware auto-instrumentation', () => { - const routeFileWithMiddleware = ` -import { createFileRoute } from '@tanstack/react-router'; -import { loggingMiddleware } from '../middleware'; + it('wraps multiple middleware entries correctly', () => { + const code = 'createStart(() => ({ requestMiddleware: [a, b, c] }));'; + const result = wrapGlobalMiddleware(code, '/app/start.ts', false); -export const Route = createFileRoute('/api/test')({ - server: { - middleware: [loggingMiddleware], - handlers: { - GET: async () => ({ message: 'test' }), - }, - }, -}); -`; + expect(result.didWrap).toBe(true); + expect(result.code).toContain('requestMiddleware: wrapMiddlewaresWithSentry({ a, b, c })'); + }); - it('instruments route-level middleware arrays', () => { - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const result = plugin.transform(routeFileWithMiddleware, '/app/routes/api.test.ts'); + it('does not wrap empty middleware arrays', () => { + const code = 'createStart(() => ({ requestMiddleware: [] }));'; + const result = wrapGlobalMiddleware(code, '/app/start.ts', false); - expect(result).not.toBeNull(); - expect(result!.code).toContain("import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react'"); - expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ loggingMiddleware })'); + expect(result.didWrap).toBe(false); + expect(result.skipped).toHaveLength(0); }); - it('instruments multiple middlewares in route file', () => { - const code = ` -import { createFileRoute } from '@tanstack/react-router'; -export const Route = createFileRoute('/foo')({ - server: { - middleware: [authMiddleware, loggingMiddleware], - handlers: { GET: () => ({}) }, - }, -}); -`; - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const result = plugin.transform(code, '/app/routes/foo.ts'); + it('does not wrap if middleware contains function calls', () => { + const code = 'createStart(() => ({ requestMiddleware: [getMiddleware()] }));'; + const result = wrapGlobalMiddleware(code, '/app/start.ts', false); - expect(result).not.toBeNull(); - expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ authMiddleware, loggingMiddleware })'); + expect(result.didWrap).toBe(false); + expect(result.skipped).toContain('requestMiddleware'); }); - it('does not instrument files without createFileRoute', () => { + it('wraps valid array and skips invalid array in same file', () => { const code = ` -const middleware = [someMiddleware]; -export const foo = { middleware }; +createStart(() => ({ + requestMiddleware: [authMiddleware], + functionMiddleware: [getMiddleware()] +})); `; - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const result = plugin.transform(code, '/app/utils.ts'); + const result = wrapGlobalMiddleware(code, '/app/start.ts', false); - expect(result).toBeNull(); + expect(result.didWrap).toBe(true); + expect(result.code).toContain('requestMiddleware: wrapMiddlewaresWithSentry({ authMiddleware })'); + expect(result.code).toContain('functionMiddleware: [getMiddleware()]'); + expect(result.skipped).toContain('functionMiddleware'); + }); + + it('handles trailing commas in middleware arrays', () => { + const code = 'createStart(() => ({ requestMiddleware: [authMiddleware,] }));'; + const result = wrapGlobalMiddleware(code, '/app/start.ts', false); + + expect(result.didWrap).toBe(true); + expect(result.code).toContain('requestMiddleware: wrapMiddlewaresWithSentry({ authMiddleware })'); + }); + + it('logs debug message when debug is enabled', () => { + const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const code = 'createStart(() => ({ requestMiddleware: [authMiddleware] }));'; + wrapGlobalMiddleware(code, '/app/start.ts', true); + + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Auto-wrapping requestMiddleware')); + + consoleLogSpy.mockRestore(); }); +}); - it('does not instrument route files without middleware arrays', () => { +describe('wrapRouteMiddleware', () => { + it('wraps route-level middleware arrays', () => { const code = ` -import { createFileRoute } from '@tanstack/react-router'; -export const Route = createFileRoute('/client')({ - component: () => '
Client only
', +export const Route = createFileRoute('/foo')({ + server: { + middleware: [loggingMiddleware], + handlers: { GET: () => ({}) }, + }, }); `; - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const result = plugin.transform(code, '/app/routes/client.tsx'); + const result = wrapRouteMiddleware(code, '/app/routes/foo.ts', false); - expect(result).toBeNull(); + expect(result.didWrap).toBe(true); + expect(result.code).toContain('middleware: wrapMiddlewaresWithSentry({ loggingMiddleware })'); + expect(result.skipped).toHaveLength(0); }); - it('does not instrument empty middleware arrays in route files', () => { + it('wraps multiple middlewares in route file', () => { const code = ` -import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/foo')({ server: { - middleware: [], + middleware: [authMiddleware, loggingMiddleware], handlers: { GET: () => ({}) }, }, }); `; - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const result = plugin.transform(code, '/app/routes/foo.ts'); + const result = wrapRouteMiddleware(code, '/app/routes/foo.ts', false); - expect(result).toBeNull(); + expect(result.didWrap).toBe(true); + expect(result.code).toContain('middleware: wrapMiddlewaresWithSentry({ authMiddleware, loggingMiddleware })'); }); -}); -describe('handler-specific middleware auto-instrumentation', () => { - it('instruments handler-level middleware arrays', () => { + it('wraps handler-level middleware arrays', () => { const code = ` -import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/foo')({ server: { handlers: ({ createHandlers }) => @@ -291,16 +250,14 @@ export const Route = createFileRoute('/foo')({ }, }); `; - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const result = plugin.transform(code, '/app/routes/foo.ts'); + const result = wrapRouteMiddleware(code, '/app/routes/foo.ts', false); - expect(result).not.toBeNull(); - expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ loggingMiddleware })'); + expect(result.didWrap).toBe(true); + expect(result.code).toContain('middleware: wrapMiddlewaresWithSentry({ loggingMiddleware })'); }); - it('instruments multiple handler-level middleware arrays in same file', () => { + it('wraps multiple handler-level middleware arrays in same file', () => { const code = ` -import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/foo')({ server: { handlers: ({ createHandlers }) => @@ -317,108 +274,81 @@ export const Route = createFileRoute('/foo')({ }, }); `; - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const result = plugin.transform(code, '/app/routes/foo.ts'); + const result = wrapRouteMiddleware(code, '/app/routes/foo.ts', false); - expect(result).not.toBeNull(); - expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ readMiddleware })'); - expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ writeMiddleware, authMiddleware })'); + expect(result.didWrap).toBe(true); + expect(result.code).toContain('middleware: wrapMiddlewaresWithSentry({ readMiddleware })'); + expect(result.code).toContain('middleware: wrapMiddlewaresWithSentry({ writeMiddleware, authMiddleware })'); }); -}); -describe('route middleware edge cases', () => { - it('does not wrap middleware containing function calls in route files', () => { + it('does not wrap empty middleware arrays', () => { const code = ` -import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/foo')({ server: { - middleware: [createMiddleware()], + middleware: [], handlers: { GET: () => ({}) }, }, }); `; - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const result = plugin.transform(code, '/app/routes/foo.ts'); + const result = wrapRouteMiddleware(code, '/app/routes/foo.ts', false); - expect(result).toBeNull(); + expect(result.didWrap).toBe(false); + expect(result.skipped).toHaveLength(0); }); - it('warns about route middleware that cannot be auto-wrapped', () => { - const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - + it('does not wrap middleware containing function calls', () => { const code = ` -import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/foo')({ server: { - middleware: [getMiddleware()], + middleware: [createMiddleware()], handlers: { GET: () => ({}) }, }, }); `; - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - plugin.transform(code, '/app/routes/foo.ts'); - - expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Could not auto-instrument route middleware')); + const result = wrapRouteMiddleware(code, '/app/routes/foo.ts', false); - consoleWarnSpy.mockRestore(); + expect(result.didWrap).toBe(false); + expect(result.skipped).toContain('route middleware'); }); - it('handles route files with use server directive', () => { - const code = `'use server'; -import { createFileRoute } from '@tanstack/react-router'; + it('wraps both route-level and handler-level middleware in same file', () => { + const code = ` export const Route = createFileRoute('/foo')({ server: { - middleware: [authMiddleware], - handlers: { GET: () => ({}) }, + middleware: [routeMiddleware], + handlers: ({ createHandlers }) => + createHandlers({ + GET: { + middleware: [getMiddleware], + handler: () => ({}), + }, + }), }, }); `; - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const result = plugin.transform(code, '/app/routes/foo.ts'); + const result = wrapRouteMiddleware(code, '/app/routes/foo.ts', false); - expect(result).not.toBeNull(); - expect(result!.code).toMatch(/^'use server';\s*\nimport \{ wrapMiddlewaresWithSentry \}/); + expect(result.didWrap).toBe(true); + expect(result.code).toContain('middleware: wrapMiddlewaresWithSentry({ routeMiddleware })'); + expect(result.code).toContain('middleware: wrapMiddlewaresWithSentry({ getMiddleware })'); }); - it('does not instrument route files that already use wrapMiddlewaresWithSentry', () => { + it('logs debug message when debug is enabled', () => { + const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const code = ` -import { createFileRoute } from '@tanstack/react-router'; -import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react'; export const Route = createFileRoute('/foo')({ server: { - middleware: wrapMiddlewaresWithSentry({ authMiddleware }), + middleware: [authMiddleware], handlers: { GET: () => ({}) }, }, }); `; - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const result = plugin.transform(code, '/app/routes/foo.ts'); + wrapRouteMiddleware(code, '/app/routes/foo.ts', true); - expect(result).toBeNull(); - }); - - it('handles both route-level and handler-level middleware in same file', () => { - const code = ` -import { createFileRoute } from '@tanstack/react-router'; -export const Route = createFileRoute('/foo')({ - server: { - middleware: [routeMiddleware], - handlers: ({ createHandlers }) => - createHandlers({ - GET: { - middleware: [getMiddleware], - handler: () => ({}), - }, - }), - }, -}); -`; - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const result = plugin.transform(code, '/app/routes/foo.ts'); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Auto-wrapping route middleware')); - expect(result).not.toBeNull(); - expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ routeMiddleware })'); - expect(result!.code).toContain('middleware: wrapMiddlewaresWithSentry({ getMiddleware })'); + consoleLogSpy.mockRestore(); }); }); From 3660c72cc84b18aadc259408685b007f5d74f74e Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 27 Jan 2026 12:11:08 +0100 Subject: [PATCH 03/14] update e2e tests --- .../tanstackstart-react/src/middleware.ts | 21 +++++++------------ .../src/routes/api.test-middleware.ts | 4 ++-- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/middleware.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/middleware.ts index 780d8a3a2a9d..60374f8fda60 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/middleware.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/middleware.ts @@ -21,8 +21,8 @@ const serverFnMiddleware = createMiddleware({ type: 'function' }).server(async ( return next(); }); -// Server route request middleware -const serverRouteRequestMiddleware = createMiddleware().server(async ({ next }) => { +// Server route request middleware - exported unwrapped for auto-instrumentation via Vite plugin +export const serverRouteRequestMiddleware = createMiddleware().server(async ({ next }) => { console.log('Server route request middleware executed'); return next(); }); @@ -40,14 +40,9 @@ const errorMiddleware = createMiddleware({ type: 'function' }).server(async () = }); // Manually wrap middlewares with Sentry (for middlewares that won't be auto-instrumented) -export const [ - wrappedServerFnMiddleware, - wrappedServerRouteRequestMiddleware, - wrappedEarlyReturnMiddleware, - wrappedErrorMiddleware, -] = wrapMiddlewaresWithSentry({ - serverFnMiddleware, - serverRouteRequestMiddleware, - earlyReturnMiddleware, - errorMiddleware, -}); +export const [wrappedServerFnMiddleware, wrappedEarlyReturnMiddleware, wrappedErrorMiddleware] = + wrapMiddlewaresWithSentry({ + serverFnMiddleware, + earlyReturnMiddleware, + errorMiddleware, + }); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.test-middleware.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.test-middleware.ts index 1bf3fdb1c5da..d24f8ed61a45 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.test-middleware.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.test-middleware.ts @@ -1,9 +1,9 @@ import { createFileRoute } from '@tanstack/react-router'; -import { wrappedServerRouteRequestMiddleware } from '../middleware'; +import { serverRouteRequestMiddleware } from '../middleware'; export const Route = createFileRoute('/api/test-middleware')({ server: { - middleware: [wrappedServerRouteRequestMiddleware], + middleware: [serverRouteRequestMiddleware], handlers: { GET: async () => { return { message: 'Server route middleware test' }; From 9a8026b1c3e6ea78b0deb35a0131a5b9a50ca656 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 27 Jan 2026 12:21:13 +0100 Subject: [PATCH 04/14] refactor --- .../src/vite/autoInstrumentMiddleware.ts | 30 +++++++---- .../vite/autoInstrumentMiddleware.test.ts | 52 ++++++++++--------- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts index f53119f350fc..68611072b6c1 100644 --- a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts +++ b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts @@ -42,6 +42,24 @@ export function wrapGlobalMiddleware(code: string, id: string, debug: boolean): return { code: transformed, didWrap, skipped }; } +/** + * Adds the wrapMiddlewaresWithSentry import to the code. + * Handles 'use client' and 'use server' directives by inserting the import after them. + */ +export function addSentryImport(code: string): string { + const sentryImport = "import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\n"; + + // Check for 'use server' or 'use client' directives, these need to be before any imports + const directiveMatch = code.match(/^(['"])use (client|server)\1;?\s*\n?/); + + if (!directiveMatch) { + return sentryImport + code; + } + + const directive = directiveMatch[0]; + return directive + sentryImport + code.slice(directive.length); +} + /** * Wraps route middleware arrays in createFileRoute() files. */ @@ -140,17 +158,7 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle return null; } - const sentryImport = "import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\n"; - - // Check for 'use server' or 'use client' directives, these need to be before any imports - const directiveMatch = transformed.match(/^(['"])use (client|server)\1;?\s*\n?/); - if (directiveMatch) { - // Insert import after the directive - const directive = directiveMatch[0]; - transformed = directive + sentryImport + transformed.slice(directive.length); - } else { - transformed = sentryImport + transformed; - } + transformed = addSentryImport(transformed); return { code: transformed, map: null }; }, diff --git a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts index 17db2a96a849..7bbbb9432bb4 100644 --- a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts +++ b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts @@ -1,6 +1,7 @@ import type { Plugin } from 'vite'; import { describe, expect, it, vi } from 'vitest'; import { + addSentryImport, arrayToObjectShorthand, makeAutoInstrumentMiddlewarePlugin, wrapGlobalMiddleware, @@ -61,30 +62,6 @@ createStart(() => ({ requestMiddleware: wrapMiddlewaresWithSentry({ myMiddleware expect(result).toBeNull(); }); - it('handles files with use server directive', () => { - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const code = `'use server'; -import { createStart } from '@tanstack/react-start'; -createStart(() => ({ requestMiddleware: [authMiddleware] })); -`; - const result = plugin.transform(code, '/app/start.ts'); - - expect(result).not.toBeNull(); - expect(result!.code).toMatch(/^'use server';\s*\nimport \{ wrapMiddlewaresWithSentry \}/); - }); - - it('handles files with use client directive', () => { - const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; - const code = `"use client"; -import { createStart } from '@tanstack/react-start'; -createStart(() => ({ requestMiddleware: [authMiddleware] })); -`; - const result = plugin.transform(code, '/app/start.ts'); - - expect(result).not.toBeNull(); - expect(result!.code).toMatch(/^"use client";\s*\nimport \{ wrapMiddlewaresWithSentry \}/); - }); - it('adds import statement when wrapping middlewares', () => { const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; const result = plugin.transform(createStartFile, '/app/start.ts'); @@ -352,6 +329,33 @@ export const Route = createFileRoute('/foo')({ }); }); +describe('addSentryImport', () => { + it('prepends import to code without directives', () => { + const code = 'const foo = 1;'; + const result = addSentryImport(code); + + expect(result).toBe( + "import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\nconst foo = 1;", + ); + }); + + it('inserts import after use server directive', () => { + const code = "'use server';\nconst foo = 1;"; + const result = addSentryImport(code); + + expect(result).toMatch(/^'use server';\nimport \{ wrapMiddlewaresWithSentry \}/); + expect(result).toContain('const foo = 1;'); + }); + + it('inserts import after use client directive', () => { + const code = '"use client";\nconst foo = 1;'; + const result = addSentryImport(code); + + expect(result).toMatch(/^"use client";\nimport \{ wrapMiddlewaresWithSentry \}/); + expect(result).toContain('const foo = 1;'); + }); +}); + describe('arrayToObjectShorthand', () => { it('converts single identifier', () => { expect(arrayToObjectShorthand('foo')).toBe('{ foo }'); From e985b3e56d6f4b5273d4cf606d05055c390c4645 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 27 Jan 2026 12:31:21 +0100 Subject: [PATCH 05/14] yarn fix --- .../src/vite/autoInstrumentMiddleware.ts | 36 +++++++++---------- .../vite/autoInstrumentMiddleware.test.ts | 4 +-- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts index 68611072b6c1..f5f146dff572 100644 --- a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts +++ b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts @@ -42,24 +42,6 @@ export function wrapGlobalMiddleware(code: string, id: string, debug: boolean): return { code: transformed, didWrap, skipped }; } -/** - * Adds the wrapMiddlewaresWithSentry import to the code. - * Handles 'use client' and 'use server' directives by inserting the import after them. - */ -export function addSentryImport(code: string): string { - const sentryImport = "import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\n"; - - // Check for 'use server' or 'use client' directives, these need to be before any imports - const directiveMatch = code.match(/^(['"])use (client|server)\1;?\s*\n?/); - - if (!directiveMatch) { - return sentryImport + code; - } - - const directive = directiveMatch[0]; - return directive + sentryImport + code.slice(directive.length); -} - /** * Wraps route middleware arrays in createFileRoute() files. */ @@ -189,3 +171,21 @@ export function arrayToObjectShorthand(contents: string): string | null { return `{ ${uniqueItems.join(', ')} }`; } + +/** + * Adds the wrapMiddlewaresWithSentry import to the code. + * Handles 'use client' and 'use server' directives by inserting the import after them. + */ +export function addSentryImport(code: string): string { + const sentryImport = "import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\n"; + + // Check for 'use server' or 'use client' directives, these need to be before any imports + const directiveMatch = code.match(/^(['"])use (client|server)\1;?\s*\n?/); + + if (!directiveMatch) { + return sentryImport + code; + } + + const directive = directiveMatch[0]; + return directive + sentryImport + code.slice(directive.length); +} diff --git a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts index 7bbbb9432bb4..6e4c4b90c7d5 100644 --- a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts +++ b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts @@ -334,9 +334,7 @@ describe('addSentryImport', () => { const code = 'const foo = 1;'; const result = addSentryImport(code); - expect(result).toBe( - "import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\nconst foo = 1;", - ); + expect(result).toBe("import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\nconst foo = 1;"); }); it('inserts import after use server directive', () => { From f7f4c22892a95f347082156091974861c34bf71b Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 27 Jan 2026 12:50:43 +0100 Subject: [PATCH 06/14] refactor --- .../src/vite/autoInstrumentMiddleware.ts | 72 +++++++------------ .../vite/autoInstrumentMiddleware.test.ts | 4 +- 2 files changed, 28 insertions(+), 48 deletions(-) diff --git a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts index f5f146dff572..c7f23c645272 100644 --- a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts +++ b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts @@ -12,65 +12,45 @@ type WrapResult = { }; /** - * Wraps global middleware arrays (requestMiddleware, functionMiddleware) in createStart() files. + * Core function that wraps middleware arrays matching the given regex. */ -export function wrapGlobalMiddleware(code: string, id: string, debug: boolean): WrapResult { +function wrapMiddlewareArrays(code: string, id: string, debug: boolean, regex: RegExp): WrapResult { const skipped: string[] = []; let didWrap = false; - const transformed = code.replace( - /(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g, - (match: string, key: string, contents: string) => { - const objContents = arrayToObjectShorthand(contents); - if (objContents) { - didWrap = true; - if (debug) { - // eslint-disable-next-line no-console - console.log(`[Sentry] Auto-wrapping ${key} in ${id}`); - } - return `${key}: wrapMiddlewaresWithSentry(${objContents})`; - } - // Track middlewares that couldn't be auto-wrapped - // Skip if we matched whitespace only - if (contents.trim()) { - skipped.push(key); + const transformed = code.replace(regex, (match: string, key: string, contents: string) => { + const objContents = arrayToObjectShorthand(contents); + if (objContents) { + didWrap = true; + if (debug) { + // eslint-disable-next-line no-console + console.log(`[Sentry] Auto-wrapping ${key} in ${id}`); } - return match; - }, - ); + return `${key}: wrapMiddlewaresWithSentry(${objContents})`; + } + // Track middlewares that couldn't be auto-wrapped + // Skip if we matched whitespace only + if (contents.trim()) { + skipped.push(key); + } + return match; + }); return { code: transformed, didWrap, skipped }; } +/** + * Wraps global middleware arrays (requestMiddleware, functionMiddleware) in createStart() files. + */ +export function wrapGlobalMiddleware(code: string, id: string, debug: boolean): WrapResult { + return wrapMiddlewareArrays(code, id, debug, /(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g); +} + /** * Wraps route middleware arrays in createFileRoute() files. */ export function wrapRouteMiddleware(code: string, id: string, debug: boolean): WrapResult { - const skipped: string[] = []; - let didWrap = false; - - const transformed = code.replace( - /(\s+)(middleware)\s*:\s*\[([^\]]*)\]/g, - (match: string, whitespace: string, key: string, contents: string) => { - const objContents = arrayToObjectShorthand(contents); - if (objContents) { - didWrap = true; - if (debug) { - // eslint-disable-next-line no-console - console.log(`[Sentry] Auto-wrapping route ${key} in ${id}`); - } - return `${whitespace}${key}: wrapMiddlewaresWithSentry(${objContents})`; - } - // Track middlewares that couldn't be auto-wrapped - // Skip if we matched whitespace only - if (contents.trim()) { - skipped.push(`route ${key}`); - } - return match; - }, - ); - - return { code: transformed, didWrap, skipped }; + return wrapMiddlewareArrays(code, id, debug, /(middleware)\s*:\s*\[([^\]]*)\]/g); } /** diff --git a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts index 6e4c4b90c7d5..124327613286 100644 --- a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts +++ b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts @@ -285,7 +285,7 @@ export const Route = createFileRoute('/foo')({ const result = wrapRouteMiddleware(code, '/app/routes/foo.ts', false); expect(result.didWrap).toBe(false); - expect(result.skipped).toContain('route middleware'); + expect(result.skipped).toContain('middleware'); }); it('wraps both route-level and handler-level middleware in same file', () => { @@ -323,7 +323,7 @@ export const Route = createFileRoute('/foo')({ `; wrapRouteMiddleware(code, '/app/routes/foo.ts', true); - expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Auto-wrapping route middleware')); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Auto-wrapping middleware')); consoleLogSpy.mockRestore(); }); From 092858fa0c553cc6d167e45da9e2ba3b94cf7857 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 27 Jan 2026 13:02:20 +0100 Subject: [PATCH 07/14] isStartFile XOR isRouteFile --- .../src/vite/autoInstrumentMiddleware.ts | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts index c7f23c645272..d99e943911be 100644 --- a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts +++ b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts @@ -92,18 +92,25 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle let needsImport = false; const skippedMiddlewares: string[] = []; - if (isStartFile) { - const result = wrapGlobalMiddleware(transformed, id, debug); - transformed = result.code; - needsImport = needsImport || result.didWrap; - skippedMiddlewares.push(...result.skipped); - } - - if (isRouteFile) { - const result = wrapRouteMiddleware(transformed, id, debug); - transformed = result.code; - needsImport = needsImport || result.didWrap; - skippedMiddlewares.push(...result.skipped); + switch (true) { + // global middleware + case isStartFile: { + const result = wrapGlobalMiddleware(transformed, id, debug); + transformed = result.code; + needsImport = needsImport || result.didWrap; + skippedMiddlewares.push(...result.skipped); + break; + } + // route middleware + case isRouteFile: { + const result = wrapRouteMiddleware(transformed, id, debug); + transformed = result.code; + needsImport = needsImport || result.didWrap; + skippedMiddlewares.push(...result.skipped); + break; + } + default: + break; } // Warn about middlewares that couldn't be auto-wrapped From ce8dbc76dd28cf6f92452003d913392aeb69a4bc Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 27 Jan 2026 16:14:25 +0100 Subject: [PATCH 08/14] guard for double import --- .../src/vite/autoInstrumentMiddleware.ts | 5 +++++ .../test/vite/autoInstrumentMiddleware.test.ts | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts index d99e943911be..a39d1384e038 100644 --- a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts +++ b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts @@ -166,6 +166,11 @@ export function arrayToObjectShorthand(contents: string): string | null { export function addSentryImport(code: string): string { const sentryImport = "import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\n"; + // Don't add the import if it already exists + if (code.includes(sentryImport.trimEnd())) { + return code; + } + // Check for 'use server' or 'use client' directives, these need to be before any imports const directiveMatch = code.match(/^(['"])use (client|server)\1;?\s*\n?/); diff --git a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts index 124327613286..4a4bfdf4fd29 100644 --- a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts +++ b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts @@ -352,6 +352,16 @@ describe('addSentryImport', () => { expect(result).toMatch(/^"use client";\nimport \{ wrapMiddlewaresWithSentry \}/); expect(result).toContain('const foo = 1;'); }); + + it('does not add import if it already exists', () => { + const code = "import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\nconst foo = 1;"; + const result = addSentryImport(code); + + expect(result).toBe(code); + // Verify the import appears exactly once + const importCount = (result.match(/import \{ wrapMiddlewaresWithSentry \}/g) || []).length; + expect(importCount).toBe(1); + }); }); describe('arrayToObjectShorthand', () => { From 330d7f89be080b8ea5107a3e100477254a142939 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 27 Jan 2026 17:22:30 +0100 Subject: [PATCH 09/14] add double wrapping test --- .../vite/autoInstrumentMiddleware.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts index 4a4bfdf4fd29..fcfbbd8d3caa 100644 --- a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts +++ b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts @@ -179,6 +179,20 @@ createStart(() => ({ consoleLogSpy.mockRestore(); }); + + it('does not double-wrap already wrapped middleware', () => { + const code = ` +createStart(() => ({ + requestMiddleware: wrapMiddlewaresWithSentry({ authMiddleware }), + functionMiddleware: wrapMiddlewaresWithSentry({ loggingMiddleware }), +})); +`; + const result = wrapGlobalMiddleware(code, '/app/start.ts', false); + + expect(result.didWrap).toBe(false); + expect(result.code).toBe(code); + expect(result.skipped).toHaveLength(0); + }); }); describe('wrapRouteMiddleware', () => { @@ -327,6 +341,22 @@ export const Route = createFileRoute('/foo')({ consoleLogSpy.mockRestore(); }); + + it('does not double-wrap already wrapped middleware', () => { + const code = ` +export const Route = createFileRoute('/foo')({ + server: { + middleware: wrapMiddlewaresWithSentry({ authMiddleware }), + handlers: { GET: () => ({}) }, + }, +}); +`; + const result = wrapRouteMiddleware(code, '/app/routes/foo.ts', false); + + expect(result.didWrap).toBe(false); + expect(result.code).toBe(code); + expect(result.skipped).toHaveLength(0); + }); }); describe('addSentryImport', () => { From b262eaaed463c998858258deab622bd52a7bd614 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 27 Jan 2026 17:23:39 +0100 Subject: [PATCH 10/14] Revert "add double wrapping test" This reverts commit 330d7f89be080b8ea5107a3e100477254a142939. --- .../vite/autoInstrumentMiddleware.test.ts | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts index fcfbbd8d3caa..4a4bfdf4fd29 100644 --- a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts +++ b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts @@ -179,20 +179,6 @@ createStart(() => ({ consoleLogSpy.mockRestore(); }); - - it('does not double-wrap already wrapped middleware', () => { - const code = ` -createStart(() => ({ - requestMiddleware: wrapMiddlewaresWithSentry({ authMiddleware }), - functionMiddleware: wrapMiddlewaresWithSentry({ loggingMiddleware }), -})); -`; - const result = wrapGlobalMiddleware(code, '/app/start.ts', false); - - expect(result.didWrap).toBe(false); - expect(result.code).toBe(code); - expect(result.skipped).toHaveLength(0); - }); }); describe('wrapRouteMiddleware', () => { @@ -341,22 +327,6 @@ export const Route = createFileRoute('/foo')({ consoleLogSpy.mockRestore(); }); - - it('does not double-wrap already wrapped middleware', () => { - const code = ` -export const Route = createFileRoute('/foo')({ - server: { - middleware: wrapMiddlewaresWithSentry({ authMiddleware }), - handlers: { GET: () => ({}) }, - }, -}); -`; - const result = wrapRouteMiddleware(code, '/app/routes/foo.ts', false); - - expect(result.didWrap).toBe(false); - expect(result.code).toBe(code); - expect(result.skipped).toHaveLength(0); - }); }); describe('addSentryImport', () => { From 14873c3b669a11d306a8f0709b8b0d807e431565 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 27 Jan 2026 15:24:44 +0100 Subject: [PATCH 11/14] feat(tanstackstart-react): Auto-instrument server function middleware --- .../tanstackstart-react/src/middleware.ts | 19 ++--- .../src/routes/test-middleware.tsx | 8 +- .../src/vite/autoInstrumentMiddleware.ts | 34 ++++++-- .../vite/autoInstrumentMiddleware.test.ts | 81 +++++++++++++++++++ 4 files changed, 118 insertions(+), 24 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/middleware.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/middleware.ts index 60374f8fda60..a15bd3744910 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/middleware.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/middleware.ts @@ -1,5 +1,4 @@ import { createMiddleware } from '@tanstack/react-start'; -import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react'; // Global request middleware - runs on every request // NOTE: This is exported unwrapped to test auto-instrumentation via the Vite plugin @@ -15,8 +14,8 @@ export const globalFunctionMiddleware = createMiddleware({ type: 'function' }).s return next(); }); -// Server function middleware -const serverFnMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => { +// Server function middleware - exported unwrapped for auto-instrumentation via Vite plugin +export const serverFnMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => { console.log('Server function middleware executed'); return next(); }); @@ -28,21 +27,15 @@ export const serverRouteRequestMiddleware = createMiddleware().server(async ({ n }); // Early return middleware - returns without calling next() -const earlyReturnMiddleware = createMiddleware({ type: 'function' }).server(async () => { +// Exported unwrapped for auto-instrumentation via Vite plugin +export const earlyReturnMiddleware = createMiddleware({ type: 'function' }).server(async () => { console.log('Early return middleware executed - not calling next()'); return { earlyReturn: true, message: 'Middleware returned early without calling next()' }; }); // Error middleware - throws an exception -const errorMiddleware = createMiddleware({ type: 'function' }).server(async () => { +// Exported unwrapped for auto-instrumentation via Vite plugin +export const errorMiddleware = createMiddleware({ type: 'function' }).server(async () => { console.log('Error middleware executed - throwing error'); throw new Error('Middleware Error Test'); }); - -// Manually wrap middlewares with Sentry (for middlewares that won't be auto-instrumented) -export const [wrappedServerFnMiddleware, wrappedEarlyReturnMiddleware, wrappedErrorMiddleware] = - wrapMiddlewaresWithSentry({ - serverFnMiddleware, - earlyReturnMiddleware, - errorMiddleware, - }); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/test-middleware.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/test-middleware.tsx index 83ac81c75a62..81d621b92a46 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/test-middleware.tsx +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/test-middleware.tsx @@ -1,10 +1,10 @@ import { createFileRoute } from '@tanstack/react-router'; import { createServerFn } from '@tanstack/react-start'; -import { wrappedServerFnMiddleware, wrappedEarlyReturnMiddleware, wrappedErrorMiddleware } from '../middleware'; +import { serverFnMiddleware, earlyReturnMiddleware, errorMiddleware } from '../middleware'; // Server function with specific middleware (also gets global function middleware) const serverFnWithMiddleware = createServerFn() - .middleware([wrappedServerFnMiddleware]) + .middleware([serverFnMiddleware]) .handler(async () => { console.log('Server function with specific middleware executed'); return { message: 'Server function middleware test' }; @@ -18,7 +18,7 @@ const serverFnWithoutMiddleware = createServerFn().handler(async () => { // Server function with early return middleware (middleware returns without calling next) const serverFnWithEarlyReturnMiddleware = createServerFn() - .middleware([wrappedEarlyReturnMiddleware]) + .middleware([earlyReturnMiddleware]) .handler(async () => { console.log('This should not be executed - middleware returned early'); return { message: 'This should not be returned' }; @@ -26,7 +26,7 @@ const serverFnWithEarlyReturnMiddleware = createServerFn() // Server function with error middleware (middleware throws an error) const serverFnWithErrorMiddleware = createServerFn() - .middleware([wrappedErrorMiddleware]) + .middleware([errorMiddleware]) .handler(async () => { console.log('This should not be executed - middleware threw error'); return { message: 'This should not be returned' }; diff --git a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts index a39d1384e038..527ae4d75471 100644 --- a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts +++ b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts @@ -26,6 +26,10 @@ function wrapMiddlewareArrays(code: string, id: string, debug: boolean, regex: R // eslint-disable-next-line no-console console.log(`[Sentry] Auto-wrapping ${key} in ${id}`); } + // Handle method call syntax like `.middleware([...])` vs object property syntax like `middleware: [...]` + if (key.endsWith('(')) { + return `${key}wrapMiddlewaresWithSentry(${objContents}))`; + } return `${key}: wrapMiddlewaresWithSentry(${objContents})`; } // Track middlewares that couldn't be auto-wrapped @@ -53,6 +57,13 @@ export function wrapRouteMiddleware(code: string, id: string, debug: boolean): W return wrapMiddlewareArrays(code, id, debug, /(middleware)\s*:\s*\[([^\]]*)\]/g); } +/** + * Wraps middleware arrays in createServerFn().middleware([...]) calls. + */ +export function wrapServerFnMiddleware(code: string, id: string, debug: boolean): WrapResult { + return wrapMiddlewareArrays(code, id, debug, /(\.middleware\s*\()\s*\[([^\]]*)\]\s*\)/g); +} + /** * A Vite plugin that automatically instruments TanStack Start middlewares: * - `requestMiddleware` and `functionMiddleware` arrays in `createStart()` @@ -78,8 +89,9 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle // Detect file types that should be instrumented const isStartFile = id.includes('start') && code.includes('createStart('); const isRouteFile = code.includes('createFileRoute(') && /middleware\s*:\s*\[/.test(code); + const isServerFnFile = code.includes('createServerFn') && /\.middleware\s*\(\s*\[/.test(code); - if (!isStartFile && !isRouteFile) { + if (!isStartFile && !isRouteFile && !isServerFnFile) { return null; } @@ -101,12 +113,20 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle skippedMiddlewares.push(...result.skipped); break; } - // route middleware - case isRouteFile: { - const result = wrapRouteMiddleware(transformed, id, debug); - transformed = result.code; - needsImport = needsImport || result.didWrap; - skippedMiddlewares.push(...result.skipped); + // non-global middleware + case (isRouteFile || isServerFnFile): { + if (isRouteFile) { + const result = wrapRouteMiddleware(transformed, id, debug); + transformed = result.code; + needsImport = needsImport || result.didWrap; + skippedMiddlewares.push(...result.skipped); + } + if (isServerFnFile) { + const result = wrapServerFnMiddleware(transformed, id, debug); + transformed = result.code; + needsImport = needsImport || result.didWrap; + skippedMiddlewares.push(...result.skipped); + } break; } default: diff --git a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts index 4a4bfdf4fd29..7a58fa3237e4 100644 --- a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts +++ b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts @@ -6,6 +6,7 @@ import { makeAutoInstrumentMiddlewarePlugin, wrapGlobalMiddleware, wrapRouteMiddleware, + wrapServerFnMiddleware, } from '../../src/vite/autoInstrumentMiddleware'; type PluginWithTransform = Plugin & { @@ -329,6 +330,86 @@ export const Route = createFileRoute('/foo')({ }); }); +describe('wrapServerFnMiddleware', () => { + it('wraps single middleware in createServerFn().middleware()', () => { + const code = ` +const serverFn = createServerFn() + .middleware([authMiddleware]) + .handler(async () => ({})); +`; + const result = wrapServerFnMiddleware(code, '/app/routes/foo.ts', false); + + expect(result.didWrap).toBe(true); + expect(result.code).toContain('.middleware(wrapMiddlewaresWithSentry({ authMiddleware }))'); + expect(result.skipped).toHaveLength(0); + }); + + it('wraps multiple middlewares in createServerFn().middleware()', () => { + const code = ` +const serverFn = createServerFn() + .middleware([authMiddleware, loggingMiddleware]) + .handler(async () => ({})); +`; + const result = wrapServerFnMiddleware(code, '/app/routes/foo.ts', false); + + expect(result.didWrap).toBe(true); + expect(result.code).toContain('.middleware(wrapMiddlewaresWithSentry({ authMiddleware, loggingMiddleware }))'); + }); + + it('does not wrap empty middleware arrays', () => { + const code = ` +const serverFn = createServerFn() + .middleware([]) + .handler(async () => ({})); +`; + const result = wrapServerFnMiddleware(code, '/app/routes/foo.ts', false); + + expect(result.didWrap).toBe(false); + expect(result.skipped).toHaveLength(0); + }); + + it('does not wrap middleware containing function calls', () => { + const code = ` +const serverFn = createServerFn() + .middleware([createMiddleware()]) + .handler(async () => ({})); +`; + const result = wrapServerFnMiddleware(code, '/app/routes/foo.ts', false); + + expect(result.didWrap).toBe(false); + expect(result.skipped).toContain('.middleware('); + }); + + it('handles multiple server functions in same file', () => { + const code = ` +const serverFn1 = createServerFn() + .middleware([authMiddleware]) + .handler(async () => ({})); + +const serverFn2 = createServerFn() + .middleware([loggingMiddleware]) + .handler(async () => ({})); +`; + const result = wrapServerFnMiddleware(code, '/app/routes/foo.ts', false); + + expect(result.didWrap).toBe(true); + expect(result.code).toContain('.middleware(wrapMiddlewaresWithSentry({ authMiddleware }))'); + expect(result.code).toContain('.middleware(wrapMiddlewaresWithSentry({ loggingMiddleware }))'); + }); + + it('handles trailing commas in middleware arrays', () => { + const code = ` +const serverFn = createServerFn() + .middleware([authMiddleware,]) + .handler(async () => ({})); +`; + const result = wrapServerFnMiddleware(code, '/app/routes/foo.ts', false); + + expect(result.didWrap).toBe(true); + expect(result.code).toContain('.middleware(wrapMiddlewaresWithSentry({ authMiddleware }))'); + }); +}); + describe('addSentryImport', () => { it('prepends import to code without directives', () => { const code = 'const foo = 1;'; From a910011ec07288080755ab0394b857ad1907bf45 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 27 Jan 2026 15:37:30 +0100 Subject: [PATCH 12/14] yarn fix --- .../tanstackstart-react/src/vite/autoInstrumentMiddleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts index 527ae4d75471..3b14a33fb770 100644 --- a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts +++ b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts @@ -114,7 +114,7 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle break; } // non-global middleware - case (isRouteFile || isServerFnFile): { + case isRouteFile || isServerFnFile: { if (isRouteFile) { const result = wrapRouteMiddleware(transformed, id, debug); transformed = result.code; From 25e86bc80c20461b0d85e8c3e09ebcab4d1039c2 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 28 Jan 2026 10:32:03 +0100 Subject: [PATCH 13/14] refactor + simplify --- .../src/vite/autoInstrumentMiddleware.ts | 79 ++++++++++--------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts index 3b14a33fb770..07cf2a29e832 100644 --- a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts +++ b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts @@ -11,6 +11,12 @@ type WrapResult = { skipped: string[]; }; +type FileTransformState = { + code: string; + needsImport: boolean; + skippedMiddlewares: string[]; +}; + /** * Core function that wraps middleware arrays matching the given regex. */ @@ -64,6 +70,23 @@ export function wrapServerFnMiddleware(code: string, id: string, debug: boolean) return wrapMiddlewareArrays(code, id, debug, /(\.middleware\s*\()\s*\[([^\]]*)\]\s*\)/g); } +/** + * Applies a wrap function to the current state and returns the updated state. + */ +function applyWrap( + state: FileTransformState, + wrapFn: (code: string, id: string, debug: boolean) => WrapResult, + id: string, + debug: boolean, +): FileTransformState { + const result = wrapFn(state.code, id, debug); + return { + code: result.code, + needsImport: state.needsImport || result.didWrap, + skippedMiddlewares: [...state.skippedMiddlewares, ...result.skipped], + }; +} + /** * A Vite plugin that automatically instruments TanStack Start middlewares: * - `requestMiddleware` and `functionMiddleware` arrays in `createStart()` @@ -100,56 +123,38 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle return null; } - let transformed = code; - let needsImport = false; - const skippedMiddlewares: string[] = []; - - switch (true) { - // global middleware - case isStartFile: { - const result = wrapGlobalMiddleware(transformed, id, debug); - transformed = result.code; - needsImport = needsImport || result.didWrap; - skippedMiddlewares.push(...result.skipped); - break; - } - // non-global middleware - case isRouteFile || isServerFnFile: { - if (isRouteFile) { - const result = wrapRouteMiddleware(transformed, id, debug); - transformed = result.code; - needsImport = needsImport || result.didWrap; - skippedMiddlewares.push(...result.skipped); - } - if (isServerFnFile) { - const result = wrapServerFnMiddleware(transformed, id, debug); - transformed = result.code; - needsImport = needsImport || result.didWrap; - skippedMiddlewares.push(...result.skipped); - } - break; - } - default: - break; + let fileTransformState: FileTransformState = { + code, + needsImport: false, + skippedMiddlewares: [], + }; + + // Wrap middlewares + if (isStartFile) { + fileTransformState = applyWrap(fileTransformState, wrapGlobalMiddleware, id, debug); + } + if (isRouteFile) { + fileTransformState = applyWrap(fileTransformState, wrapRouteMiddleware, id, debug); + } + if (isServerFnFile) { + fileTransformState = applyWrap(fileTransformState, wrapServerFnMiddleware, id, debug); } // Warn about middlewares that couldn't be auto-wrapped - if (skippedMiddlewares.length > 0) { + if (fileTransformState.skippedMiddlewares.length > 0) { // eslint-disable-next-line no-console console.warn( - `[Sentry] Could not auto-instrument ${skippedMiddlewares.join(' and ')} in ${id}. ` + + `[Sentry] Could not auto-instrument ${fileTransformState.skippedMiddlewares.join(' and ')} in ${id}. ` + 'To instrument these middlewares, use wrapMiddlewaresWithSentry() manually. ', ); } // We didn't wrap any middlewares, so we don't need to import the wrapMiddlewaresWithSentry function - if (!needsImport) { + if (!fileTransformState.needsImport) { return null; } - transformed = addSentryImport(transformed); - - return { code: transformed, map: null }; + return { code: addSentryImport(fileTransformState.code), map: null }; }, }; } From 1b2c92f361325c7aaaa28ad78bf8aa1de6764466 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 28 Jan 2026 11:38:35 +0100 Subject: [PATCH 14/14] Add changelog entry --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93a81f3fb367..f8ed44122808 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## Unreleased +### Important Changes + +- **feat(tanstackstart-react): Auto-instrument request middleware ([#18989](https://github.com/getsentry/sentry-javascript/pull/18989))** + + The `sentryTanstackStart` Vite plugin now automatically instruments `middleware` arrays in `createFileRoute()`. This captures performance data without requiring manual wrapping with `wrapMiddlewaresWithSentry()`. + +- **feat(tanstackstart-react): Auto-instrument server function middleware ([#19001](https://github.com/getsentry/sentry-javascript/pull/19001))** + + The `sentryTanstackStart` Vite plugin now automatically instruments middleware in `createServerFn().middleware([...])` calls. This captures performance data without requiring manual wrapping with `wrapMiddlewaresWithSentry()`. + +### Other Changes + - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott Work in this release was contributed by @harshit078. Thank you for your contribution!