Skip to content

Commit 33518af

Browse files
authored
feat(tanstackstart-react): Auto-instrument server function middleware (#19001)
Extending auto-instrumentation to non-global server function middleware. [TSS function middleware docs](https://tanstack.com/start/latest/docs/framework/react/guide/middleware#using-server-function-middleware) Closes #18847
1 parent 5dfc086 commit 33518af

File tree

5 files changed

+151
-48
lines changed

5 files changed

+151
-48
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
66

7+
- **feat(tanstackstart-react): Auto-instrument server function middleware ([#19001](https://github.com/getsentry/sentry-javascript/pull/19001))**
8+
9+
The `sentryTanstackStart` Vite plugin now automatically instruments middleware in `createServerFn().middleware([...])` calls. This captures performance data without requiring manual wrapping with `wrapMiddlewaresWithSentry()`.
10+
711
## 10.38.0
812

913
### Important Changes
1014

1115
- **feat(tanstackstart-react): Auto-instrument request middleware ([#18989](https://github.com/getsentry/sentry-javascript/pull/18989))**
1216

13-
The `sentryTanstackStart` Vite plugin now automatically instruments `middleware` arrays in `createFileRoute()`. This captures performance data without requiring manual wrapping with `wrapMiddlewaresWithSentry()`.
17+
The `sentryTanstackStart` Vite plugin now automatically instruments `middleware` arrays in `createFileRoute()`. This captures performance data without requiring manual wrapping with `wrapMiddlewaresWithSentry()`.
1418

1519
### Other Changes
1620

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: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ type WrapResult = {
1111
skipped: string[];
1212
};
1313

14+
type FileTransformState = {
15+
code: string;
16+
needsImport: boolean;
17+
skippedMiddlewares: string[];
18+
};
19+
1420
/**
1521
* Core function that wraps middleware arrays matching the given regex.
1622
*/
@@ -26,6 +32,10 @@ function wrapMiddlewareArrays(code: string, id: string, debug: boolean, regex: R
2632
// eslint-disable-next-line no-console
2733
console.log(`[Sentry] Auto-wrapping ${key} in ${id}`);
2834
}
35+
// Handle method call syntax like `.middleware([...])` vs object property syntax like `middleware: [...]`
36+
if (key.endsWith('(')) {
37+
return `${key}wrapMiddlewaresWithSentry(${objContents}))`;
38+
}
2939
return `${key}: wrapMiddlewaresWithSentry(${objContents})`;
3040
}
3141
// Track middlewares that couldn't be auto-wrapped
@@ -53,6 +63,30 @@ export function wrapRouteMiddleware(code: string, id: string, debug: boolean): W
5363
return wrapMiddlewareArrays(code, id, debug, /(middleware)\s*:\s*\[([^\]]*)\]/g);
5464
}
5565

66+
/**
67+
* Wraps middleware arrays in createServerFn().middleware([...]) calls.
68+
*/
69+
export function wrapServerFnMiddleware(code: string, id: string, debug: boolean): WrapResult {
70+
return wrapMiddlewareArrays(code, id, debug, /(\.middleware\s*\()\s*\[([^\]]*)\]\s*\)/g);
71+
}
72+
73+
/**
74+
* Applies a wrap function to the current state and returns the updated state.
75+
*/
76+
function applyWrap(
77+
state: FileTransformState,
78+
wrapFn: (code: string, id: string, debug: boolean) => WrapResult,
79+
id: string,
80+
debug: boolean,
81+
): FileTransformState {
82+
const result = wrapFn(state.code, id, debug);
83+
return {
84+
code: result.code,
85+
needsImport: state.needsImport || result.didWrap,
86+
skippedMiddlewares: [...state.skippedMiddlewares, ...result.skipped],
87+
};
88+
}
89+
5690
/**
5791
* A Vite plugin that automatically instruments TanStack Start middlewares:
5892
* - `requestMiddleware` and `functionMiddleware` arrays in `createStart()`
@@ -78,8 +112,9 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle
78112
// Detect file types that should be instrumented
79113
const isStartFile = id.includes('start') && code.includes('createStart(');
80114
const isRouteFile = code.includes('createFileRoute(') && /middleware\s*:\s*\[/.test(code);
115+
const isServerFnFile = code.includes('createServerFn') && /\.middleware\s*\(\s*\[/.test(code);
81116

82-
if (!isStartFile && !isRouteFile) {
117+
if (!isStartFile && !isRouteFile && !isServerFnFile) {
83118
return null;
84119
}
85120

@@ -88,48 +123,38 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle
88123
return null;
89124
}
90125

91-
let transformed = code;
92-
let needsImport = false;
93-
const skippedMiddlewares: string[] = [];
94-
95-
switch (true) {
96-
// global middleware
97-
case isStartFile: {
98-
const result = wrapGlobalMiddleware(transformed, id, debug);
99-
transformed = result.code;
100-
needsImport = needsImport || result.didWrap;
101-
skippedMiddlewares.push(...result.skipped);
102-
break;
103-
}
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);
110-
break;
111-
}
112-
default:
113-
break;
126+
let fileTransformState: FileTransformState = {
127+
code,
128+
needsImport: false,
129+
skippedMiddlewares: [],
130+
};
131+
132+
// Wrap middlewares
133+
if (isStartFile) {
134+
fileTransformState = applyWrap(fileTransformState, wrapGlobalMiddleware, id, debug);
135+
}
136+
if (isRouteFile) {
137+
fileTransformState = applyWrap(fileTransformState, wrapRouteMiddleware, id, debug);
138+
}
139+
if (isServerFnFile) {
140+
fileTransformState = applyWrap(fileTransformState, wrapServerFnMiddleware, id, debug);
114141
}
115142

116143
// Warn about middlewares that couldn't be auto-wrapped
117-
if (skippedMiddlewares.length > 0) {
144+
if (fileTransformState.skippedMiddlewares.length > 0) {
118145
// eslint-disable-next-line no-console
119146
console.warn(
120-
`[Sentry] Could not auto-instrument ${skippedMiddlewares.join(' and ')} in ${id}. ` +
147+
`[Sentry] Could not auto-instrument ${fileTransformState.skippedMiddlewares.join(' and ')} in ${id}. ` +
121148
'To instrument these middlewares, use wrapMiddlewaresWithSentry() manually. ',
122149
);
123150
}
124151

125152
// We didn't wrap any middlewares, so we don't need to import the wrapMiddlewaresWithSentry function
126-
if (!needsImport) {
153+
if (!fileTransformState.needsImport) {
127154
return null;
128155
}
129156

130-
transformed = addSentryImport(transformed);
131-
132-
return { code: transformed, map: null };
157+
return { code: addSentryImport(fileTransformState.code), map: null };
133158
},
134159
};
135160
}

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)