Skip to content

Commit 5886804

Browse files
authored
feat(tanstackstart-react): Auto-instrument request middleware (#18989)
Follow up to #18844 Extending auto-instrumentation to non-global server request middleware. [TSS request middleware docs](https://tanstack.com/start/latest/docs/framework/react/guide/middleware#request-middleware) Closes #18846
1 parent 809d578 commit 5886804

File tree

4 files changed

+383
-153
lines changed

4 files changed

+383
-153
lines changed

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

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ const serverFnMiddleware = createMiddleware({ type: 'function' }).server(async (
2121
return next();
2222
});
2323

24-
// Server route request middleware
25-
const serverRouteRequestMiddleware = createMiddleware().server(async ({ next }) => {
24+
// Server route request middleware - exported unwrapped for auto-instrumentation via Vite plugin
25+
export const serverRouteRequestMiddleware = createMiddleware().server(async ({ next }) => {
2626
console.log('Server route request middleware executed');
2727
return next();
2828
});
@@ -40,14 +40,9 @@ const errorMiddleware = createMiddleware({ type: 'function' }).server(async () =
4040
});
4141

4242
// Manually wrap middlewares with Sentry (for middlewares that won't be auto-instrumented)
43-
export const [
44-
wrappedServerFnMiddleware,
45-
wrappedServerRouteRequestMiddleware,
46-
wrappedEarlyReturnMiddleware,
47-
wrappedErrorMiddleware,
48-
] = wrapMiddlewaresWithSentry({
49-
serverFnMiddleware,
50-
serverRouteRequestMiddleware,
51-
earlyReturnMiddleware,
52-
errorMiddleware,
53-
});
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/api.test-middleware.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { createFileRoute } from '@tanstack/react-router';
2-
import { wrappedServerRouteRequestMiddleware } from '../middleware';
2+
import { serverRouteRequestMiddleware } from '../middleware';
33

44
export const Route = createFileRoute('/api/test-middleware')({
55
server: {
6-
middleware: [wrappedServerRouteRequestMiddleware],
6+
middleware: [serverRouteRequestMiddleware],
77
handlers: {
88
GET: async () => {
99
return { message: 'Server route middleware test' };

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

Lines changed: 100 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,58 @@ type AutoInstrumentMiddlewareOptions = {
55
debug?: boolean;
66
};
77

8+
type WrapResult = {
9+
code: string;
10+
didWrap: boolean;
11+
skipped: string[];
12+
};
13+
14+
/**
15+
* Core function that wraps middleware arrays matching the given regex.
16+
*/
17+
function wrapMiddlewareArrays(code: string, id: string, debug: boolean, regex: RegExp): WrapResult {
18+
const skipped: string[] = [];
19+
let didWrap = false;
20+
21+
const transformed = code.replace(regex, (match: string, key: string, contents: string) => {
22+
const objContents = arrayToObjectShorthand(contents);
23+
if (objContents) {
24+
didWrap = true;
25+
if (debug) {
26+
// eslint-disable-next-line no-console
27+
console.log(`[Sentry] Auto-wrapping ${key} in ${id}`);
28+
}
29+
return `${key}: wrapMiddlewaresWithSentry(${objContents})`;
30+
}
31+
// Track middlewares that couldn't be auto-wrapped
32+
// Skip if we matched whitespace only
33+
if (contents.trim()) {
34+
skipped.push(key);
35+
}
36+
return match;
37+
});
38+
39+
return { code: transformed, didWrap, skipped };
40+
}
41+
42+
/**
43+
* Wraps global middleware arrays (requestMiddleware, functionMiddleware) in createStart() files.
44+
*/
45+
export function wrapGlobalMiddleware(code: string, id: string, debug: boolean): WrapResult {
46+
return wrapMiddlewareArrays(code, id, debug, /(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g);
47+
}
48+
49+
/**
50+
* Wraps route middleware arrays in createFileRoute() files.
51+
*/
52+
export function wrapRouteMiddleware(code: string, id: string, debug: boolean): WrapResult {
53+
return wrapMiddlewareArrays(code, id, debug, /(middleware)\s*:\s*\[([^\]]*)\]/g);
54+
}
55+
856
/**
9-
* A Vite plugin that automatically instruments TanStack Start middlewares
10-
* by wrapping `requestMiddleware` and `functionMiddleware` arrays in `createStart()`.
57+
* A Vite plugin that automatically instruments TanStack Start middlewares:
58+
* - `requestMiddleware` and `functionMiddleware` arrays in `createStart()`
59+
* - `middleware` arrays in `createFileRoute()` route definitions
1160
*/
1261
export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddlewareOptions = {}): Plugin {
1362
const { enabled = true, debug = false } = options;
@@ -26,9 +75,11 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle
2675
return null;
2776
}
2877

29-
// Only wrap requestMiddleware and functionMiddleware in createStart()
30-
// createStart() should always be in a file named start.ts
31-
if (!id.includes('start') || !code.includes('createStart(')) {
78+
// Detect file types that should be instrumented
79+
const isStartFile = id.includes('start') && code.includes('createStart(');
80+
const isRouteFile = code.includes('createFileRoute(') && /middleware\s*:\s*\[/.test(code);
81+
82+
if (!isStartFile && !isRouteFile) {
3283
return null;
3384
}
3485

@@ -41,26 +92,26 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle
4192
let needsImport = false;
4293
const skippedMiddlewares: string[] = [];
4394

44-
transformed = transformed.replace(
45-
/(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g,
46-
(match: string, key: string, contents: string) => {
47-
const objContents = arrayToObjectShorthand(contents);
48-
if (objContents) {
49-
needsImport = true;
50-
if (debug) {
51-
// eslint-disable-next-line no-console
52-
console.log(`[Sentry] Auto-wrapping ${key} in ${id}`);
53-
}
54-
return `${key}: wrapMiddlewaresWithSentry(${objContents})`;
55-
}
56-
// Track middlewares that couldn't be auto-wrapped
57-
// Skip if we matched whitespace only
58-
if (contents.trim()) {
59-
skippedMiddlewares.push(key);
60-
}
61-
return match;
62-
},
63-
);
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;
114+
}
64115

65116
// Warn about middlewares that couldn't be auto-wrapped
66117
if (skippedMiddlewares.length > 0) {
@@ -76,17 +127,7 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle
76127
return null;
77128
}
78129

79-
const sentryImport = "import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\n";
80-
81-
// Check for 'use server' or 'use client' directives, these need to be before any imports
82-
const directiveMatch = transformed.match(/^(['"])use (client|server)\1;?\s*\n?/);
83-
if (directiveMatch) {
84-
// Insert import after the directive
85-
const directive = directiveMatch[0];
86-
transformed = directive + sentryImport + transformed.slice(directive.length);
87-
} else {
88-
transformed = sentryImport + transformed;
89-
}
130+
transformed = addSentryImport(transformed);
90131

91132
return { code: transformed, map: null };
92133
},
@@ -117,3 +158,26 @@ export function arrayToObjectShorthand(contents: string): string | null {
117158

118159
return `{ ${uniqueItems.join(', ')} }`;
119160
}
161+
162+
/**
163+
* Adds the wrapMiddlewaresWithSentry import to the code.
164+
* Handles 'use client' and 'use server' directives by inserting the import after them.
165+
*/
166+
export function addSentryImport(code: string): string {
167+
const sentryImport = "import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\n";
168+
169+
// Don't add the import if it already exists
170+
if (code.includes(sentryImport.trimEnd())) {
171+
return code;
172+
}
173+
174+
// Check for 'use server' or 'use client' directives, these need to be before any imports
175+
const directiveMatch = code.match(/^(['"])use (client|server)\1;?\s*\n?/);
176+
177+
if (!directiveMatch) {
178+
return sentryImport + code;
179+
}
180+
181+
const directive = directiveMatch[0];
182+
return directive + sentryImport + code.slice(directive.length);
183+
}

0 commit comments

Comments
 (0)