Skip to content

Commit 14873c3

Browse files
committed
feat(tanstackstart-react): Auto-instrument server function middleware
1 parent b262eaa commit 14873c3

File tree

4 files changed

+118
-24
lines changed

4 files changed

+118
-24
lines changed
Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { createMiddleware } from '@tanstack/react-start';
2-
import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';
32

43
// Global request middleware - runs on every request
54
// NOTE: This is exported unwrapped to test auto-instrumentation via the Vite plugin
@@ -15,8 +14,8 @@ export const globalFunctionMiddleware = createMiddleware({ type: 'function' }).s
1514
return next();
1615
});
1716

18-
// Server function middleware
19-
const serverFnMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => {
17+
// Server function middleware - exported unwrapped for auto-instrumentation via Vite plugin
18+
export const serverFnMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => {
2019
console.log('Server function middleware executed');
2120
return next();
2221
});
@@ -28,21 +27,15 @@ export const serverRouteRequestMiddleware = createMiddleware().server(async ({ n
2827
});
2928

3029
// Early return middleware - returns without calling next()
31-
const earlyReturnMiddleware = createMiddleware({ type: 'function' }).server(async () => {
30+
// Exported unwrapped for auto-instrumentation via Vite plugin
31+
export const earlyReturnMiddleware = createMiddleware({ type: 'function' }).server(async () => {
3232
console.log('Early return middleware executed - not calling next()');
3333
return { earlyReturn: true, message: 'Middleware returned early without calling next()' };
3434
});
3535

3636
// Error middleware - throws an exception
37-
const errorMiddleware = createMiddleware({ type: 'function' }).server(async () => {
37+
// Exported unwrapped for auto-instrumentation via Vite plugin
38+
export const errorMiddleware = createMiddleware({ type: 'function' }).server(async () => {
3839
console.log('Error middleware executed - throwing error');
3940
throw new Error('Middleware Error Test');
4041
});
41-
42-
// Manually wrap middlewares with Sentry (for middlewares that won't be auto-instrumented)
43-
export const [wrappedServerFnMiddleware, wrappedEarlyReturnMiddleware, wrappedErrorMiddleware] =
44-
wrapMiddlewaresWithSentry({
45-
serverFnMiddleware,
46-
earlyReturnMiddleware,
47-
errorMiddleware,
48-
});

dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/test-middleware.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { createFileRoute } from '@tanstack/react-router';
22
import { createServerFn } from '@tanstack/react-start';
3-
import { wrappedServerFnMiddleware, wrappedEarlyReturnMiddleware, wrappedErrorMiddleware } from '../middleware';
3+
import { serverFnMiddleware, earlyReturnMiddleware, errorMiddleware } from '../middleware';
44

55
// Server function with specific middleware (also gets global function middleware)
66
const serverFnWithMiddleware = createServerFn()
7-
.middleware([wrappedServerFnMiddleware])
7+
.middleware([serverFnMiddleware])
88
.handler(async () => {
99
console.log('Server function with specific middleware executed');
1010
return { message: 'Server function middleware test' };
@@ -18,15 +18,15 @@ const serverFnWithoutMiddleware = createServerFn().handler(async () => {
1818

1919
// Server function with early return middleware (middleware returns without calling next)
2020
const serverFnWithEarlyReturnMiddleware = createServerFn()
21-
.middleware([wrappedEarlyReturnMiddleware])
21+
.middleware([earlyReturnMiddleware])
2222
.handler(async () => {
2323
console.log('This should not be executed - middleware returned early');
2424
return { message: 'This should not be returned' };
2525
});
2626

2727
// Server function with error middleware (middleware throws an error)
2828
const serverFnWithErrorMiddleware = createServerFn()
29-
.middleware([wrappedErrorMiddleware])
29+
.middleware([errorMiddleware])
3030
.handler(async () => {
3131
console.log('This should not be executed - middleware threw error');
3232
return { message: 'This should not be returned' };

packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ function wrapMiddlewareArrays(code: string, id: string, debug: boolean, regex: R
2626
// eslint-disable-next-line no-console
2727
console.log(`[Sentry] Auto-wrapping ${key} in ${id}`);
2828
}
29+
// Handle method call syntax like `.middleware([...])` vs object property syntax like `middleware: [...]`
30+
if (key.endsWith('(')) {
31+
return `${key}wrapMiddlewaresWithSentry(${objContents}))`;
32+
}
2933
return `${key}: wrapMiddlewaresWithSentry(${objContents})`;
3034
}
3135
// Track middlewares that couldn't be auto-wrapped
@@ -53,6 +57,13 @@ export function wrapRouteMiddleware(code: string, id: string, debug: boolean): W
5357
return wrapMiddlewareArrays(code, id, debug, /(middleware)\s*:\s*\[([^\]]*)\]/g);
5458
}
5559

60+
/**
61+
* Wraps middleware arrays in createServerFn().middleware([...]) calls.
62+
*/
63+
export function wrapServerFnMiddleware(code: string, id: string, debug: boolean): WrapResult {
64+
return wrapMiddlewareArrays(code, id, debug, /(\.middleware\s*\()\s*\[([^\]]*)\]\s*\)/g);
65+
}
66+
5667
/**
5768
* A Vite plugin that automatically instruments TanStack Start middlewares:
5869
* - `requestMiddleware` and `functionMiddleware` arrays in `createStart()`
@@ -78,8 +89,9 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle
7889
// Detect file types that should be instrumented
7990
const isStartFile = id.includes('start') && code.includes('createStart(');
8091
const isRouteFile = code.includes('createFileRoute(') && /middleware\s*:\s*\[/.test(code);
92+
const isServerFnFile = code.includes('createServerFn') && /\.middleware\s*\(\s*\[/.test(code);
8193

82-
if (!isStartFile && !isRouteFile) {
94+
if (!isStartFile && !isRouteFile && !isServerFnFile) {
8395
return null;
8496
}
8597

@@ -101,12 +113,20 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle
101113
skippedMiddlewares.push(...result.skipped);
102114
break;
103115
}
104-
// route middleware
105-
case isRouteFile: {
106-
const result = wrapRouteMiddleware(transformed, id, debug);
107-
transformed = result.code;
108-
needsImport = needsImport || result.didWrap;
109-
skippedMiddlewares.push(...result.skipped);
116+
// non-global middleware
117+
case (isRouteFile || isServerFnFile): {
118+
if (isRouteFile) {
119+
const result = wrapRouteMiddleware(transformed, id, debug);
120+
transformed = result.code;
121+
needsImport = needsImport || result.didWrap;
122+
skippedMiddlewares.push(...result.skipped);
123+
}
124+
if (isServerFnFile) {
125+
const result = wrapServerFnMiddleware(transformed, id, debug);
126+
transformed = result.code;
127+
needsImport = needsImport || result.didWrap;
128+
skippedMiddlewares.push(...result.skipped);
129+
}
110130
break;
111131
}
112132
default:

packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
makeAutoInstrumentMiddlewarePlugin,
77
wrapGlobalMiddleware,
88
wrapRouteMiddleware,
9+
wrapServerFnMiddleware,
910
} from '../../src/vite/autoInstrumentMiddleware';
1011

1112
type PluginWithTransform = Plugin & {
@@ -329,6 +330,86 @@ export const Route = createFileRoute('/foo')({
329330
});
330331
});
331332

333+
describe('wrapServerFnMiddleware', () => {
334+
it('wraps single middleware in createServerFn().middleware()', () => {
335+
const code = `
336+
const serverFn = createServerFn()
337+
.middleware([authMiddleware])
338+
.handler(async () => ({}));
339+
`;
340+
const result = wrapServerFnMiddleware(code, '/app/routes/foo.ts', false);
341+
342+
expect(result.didWrap).toBe(true);
343+
expect(result.code).toContain('.middleware(wrapMiddlewaresWithSentry({ authMiddleware }))');
344+
expect(result.skipped).toHaveLength(0);
345+
});
346+
347+
it('wraps multiple middlewares in createServerFn().middleware()', () => {
348+
const code = `
349+
const serverFn = createServerFn()
350+
.middleware([authMiddleware, loggingMiddleware])
351+
.handler(async () => ({}));
352+
`;
353+
const result = wrapServerFnMiddleware(code, '/app/routes/foo.ts', false);
354+
355+
expect(result.didWrap).toBe(true);
356+
expect(result.code).toContain('.middleware(wrapMiddlewaresWithSentry({ authMiddleware, loggingMiddleware }))');
357+
});
358+
359+
it('does not wrap empty middleware arrays', () => {
360+
const code = `
361+
const serverFn = createServerFn()
362+
.middleware([])
363+
.handler(async () => ({}));
364+
`;
365+
const result = wrapServerFnMiddleware(code, '/app/routes/foo.ts', false);
366+
367+
expect(result.didWrap).toBe(false);
368+
expect(result.skipped).toHaveLength(0);
369+
});
370+
371+
it('does not wrap middleware containing function calls', () => {
372+
const code = `
373+
const serverFn = createServerFn()
374+
.middleware([createMiddleware()])
375+
.handler(async () => ({}));
376+
`;
377+
const result = wrapServerFnMiddleware(code, '/app/routes/foo.ts', false);
378+
379+
expect(result.didWrap).toBe(false);
380+
expect(result.skipped).toContain('.middleware(');
381+
});
382+
383+
it('handles multiple server functions in same file', () => {
384+
const code = `
385+
const serverFn1 = createServerFn()
386+
.middleware([authMiddleware])
387+
.handler(async () => ({}));
388+
389+
const serverFn2 = createServerFn()
390+
.middleware([loggingMiddleware])
391+
.handler(async () => ({}));
392+
`;
393+
const result = wrapServerFnMiddleware(code, '/app/routes/foo.ts', false);
394+
395+
expect(result.didWrap).toBe(true);
396+
expect(result.code).toContain('.middleware(wrapMiddlewaresWithSentry({ authMiddleware }))');
397+
expect(result.code).toContain('.middleware(wrapMiddlewaresWithSentry({ loggingMiddleware }))');
398+
});
399+
400+
it('handles trailing commas in middleware arrays', () => {
401+
const code = `
402+
const serverFn = createServerFn()
403+
.middleware([authMiddleware,])
404+
.handler(async () => ({}));
405+
`;
406+
const result = wrapServerFnMiddleware(code, '/app/routes/foo.ts', false);
407+
408+
expect(result.didWrap).toBe(true);
409+
expect(result.code).toContain('.middleware(wrapMiddlewaresWithSentry({ authMiddleware }))');
410+
});
411+
});
412+
332413
describe('addSentryImport', () => {
333414
it('prepends import to code without directives', () => {
334415
const code = 'const foo = 1;';

0 commit comments

Comments
 (0)