Skip to content

Commit 83445eb

Browse files
authored
feat(tanstackstart-react): Add wrappers for manual instrumentation of servers-side middlewares (#18680)
This PR adds a middleware wrapper to the `tanstackstart` SDK that allows users to add tracing to their application [middleware](https://tanstack.com/start/latest/docs/framework/react/guide/middleware). Eventually we will want to patch this automatically, but that is a bit tricky since it requires build-time magic. This API provides a manual alternative for now and can later still act as a fallback for cases where auto-instrumentation doesn't work. **How it works** The wrapper patches the middleware `options.server` function that gets executed whenever a middleware is run. Each middleware invocation creates a span with: - op: middleware.tanstackstart - origin: manual.middleware.tanstackstart - name: The instrumentation automatically assigns the middleware name based on the variable name assigned to the middleware. At first I had the issue that if multiple middlewares were used they would be nested (i.e. first middleware is parent of second etc.). This is because the middlewares call `next()` to move down the middleware chain, so trivially starting a span for the middleware execution would actually create a span that would last for the current middleware and any middlewares that come after in the middleware chain. I fixed that by also proxying `next()`, where I end the middleware span and then also reattach the middleware spans to the parent request span instead of the previous middleware span. **Usage** ``` import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react'; const [wrappedAuth, wrappedLogging] = wrapMiddlewaresWithSentry({ authMiddleware, loggingMiddleware, }); ``` **Tests** Added E2E tests for: - if multiple middlewares are executed we get spans for both and they are sibling spans (i.e. children of the same parent) - global request middleware - global function middleware - request middleware - middleware that throws an exception - middleware that does not call `next()` Closes #18666
1 parent 090d08c commit 83445eb

File tree

15 files changed

+527
-9
lines changed

15 files changed

+527
-9
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@
44

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

7+
- **feat(tanstackstart-react): Add `wrapMiddlewaresWithSentry` for manual middleware instrumentation**
8+
9+
You can now wrap your middlewares using `wrapMiddlewaresWithSentry`, allowing you to trace middleware execution in your TanStack Start application.
10+
11+
```ts
12+
import { createMiddleware } from '@tanstack/react-start';
13+
import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';
14+
15+
const loggingMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => {
16+
console.log('Request started');
17+
return next();
18+
});
19+
20+
export const [wrappedLoggingMiddleware] = wrapMiddlewaresWithSentry({ loggingMiddleware });
21+
```
22+
723
## 10.33.0
824

925
### Important Changes
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { createMiddleware } from '@tanstack/react-start';
2+
import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';
3+
4+
// Global request middleware - runs on every request
5+
const globalRequestMiddleware = createMiddleware().server(async ({ next }) => {
6+
console.log('Global request middleware executed');
7+
return next();
8+
});
9+
10+
// Global function middleware - runs on every server function
11+
const globalFunctionMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => {
12+
console.log('Global function middleware executed');
13+
return next();
14+
});
15+
16+
// Server function middleware
17+
const serverFnMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => {
18+
console.log('Server function middleware executed');
19+
return next();
20+
});
21+
22+
// Server route request middleware
23+
const serverRouteRequestMiddleware = createMiddleware().server(async ({ next }) => {
24+
console.log('Server route request middleware executed');
25+
return next();
26+
});
27+
28+
// Early return middleware - returns without calling next()
29+
const earlyReturnMiddleware = createMiddleware({ type: 'function' }).server(async () => {
30+
console.log('Early return middleware executed - not calling next()');
31+
return { earlyReturn: true, message: 'Middleware returned early without calling next()' };
32+
});
33+
34+
// Error middleware - throws an exception
35+
const errorMiddleware = createMiddleware({ type: 'function' }).server(async () => {
36+
console.log('Error middleware executed - throwing error');
37+
throw new Error('Middleware Error Test');
38+
});
39+
40+
// Manually wrap middlewares with Sentry
41+
export const [
42+
wrappedGlobalRequestMiddleware,
43+
wrappedGlobalFunctionMiddleware,
44+
wrappedServerFnMiddleware,
45+
wrappedServerRouteRequestMiddleware,
46+
wrappedEarlyReturnMiddleware,
47+
wrappedErrorMiddleware,
48+
] = wrapMiddlewaresWithSentry({
49+
globalRequestMiddleware,
50+
globalFunctionMiddleware,
51+
serverFnMiddleware,
52+
serverRouteRequestMiddleware,
53+
earlyReturnMiddleware,
54+
errorMiddleware,
55+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { createFileRoute } from '@tanstack/react-router';
2+
import { wrappedServerRouteRequestMiddleware } from '../middleware';
3+
4+
export const Route = createFileRoute('/api/test-middleware')({
5+
server: {
6+
middleware: [wrappedServerRouteRequestMiddleware],
7+
handlers: {
8+
GET: async () => {
9+
return { message: 'Server route middleware test' };
10+
},
11+
},
12+
},
13+
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { createFileRoute } from '@tanstack/react-router';
2+
import { createServerFn } from '@tanstack/react-start';
3+
import { wrappedServerFnMiddleware, wrappedEarlyReturnMiddleware, wrappedErrorMiddleware } from '../middleware';
4+
5+
// Server function with specific middleware (also gets global function middleware)
6+
const serverFnWithMiddleware = createServerFn()
7+
.middleware([wrappedServerFnMiddleware])
8+
.handler(async () => {
9+
console.log('Server function with specific middleware executed');
10+
return { message: 'Server function middleware test' };
11+
});
12+
13+
// Server function without specific middleware (only gets global function middleware)
14+
const serverFnWithoutMiddleware = createServerFn().handler(async () => {
15+
console.log('Server function without specific middleware executed');
16+
return { message: 'Global middleware only test' };
17+
});
18+
19+
// Server function with early return middleware (middleware returns without calling next)
20+
const serverFnWithEarlyReturnMiddleware = createServerFn()
21+
.middleware([wrappedEarlyReturnMiddleware])
22+
.handler(async () => {
23+
console.log('This should not be executed - middleware returned early');
24+
return { message: 'This should not be returned' };
25+
});
26+
27+
// Server function with error middleware (middleware throws an error)
28+
const serverFnWithErrorMiddleware = createServerFn()
29+
.middleware([wrappedErrorMiddleware])
30+
.handler(async () => {
31+
console.log('This should not be executed - middleware threw error');
32+
return { message: 'This should not be returned' };
33+
});
34+
35+
export const Route = createFileRoute('/test-middleware')({
36+
component: TestMiddleware,
37+
});
38+
39+
function TestMiddleware() {
40+
return (
41+
<div>
42+
<h1>Test Middleware Page</h1>
43+
<button
44+
id="server-fn-middleware-btn"
45+
type="button"
46+
onClick={async () => {
47+
await serverFnWithMiddleware();
48+
}}
49+
>
50+
Call server function with middleware
51+
</button>
52+
<button
53+
id="server-fn-global-only-btn"
54+
type="button"
55+
onClick={async () => {
56+
await serverFnWithoutMiddleware();
57+
}}
58+
>
59+
Call server function (global middleware only)
60+
</button>
61+
<button
62+
id="server-fn-early-return-btn"
63+
type="button"
64+
onClick={async () => {
65+
const result = await serverFnWithEarlyReturnMiddleware();
66+
console.log('Early return result:', result);
67+
}}
68+
>
69+
Call server function with early return middleware
70+
</button>
71+
<button
72+
id="server-fn-error-btn"
73+
type="button"
74+
onClick={async () => {
75+
try {
76+
await serverFnWithErrorMiddleware();
77+
} catch (error) {
78+
console.log('Caught error from middleware:', error);
79+
}
80+
}}
81+
>
82+
Call server function with error middleware
83+
</button>
84+
</div>
85+
);
86+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createStart } from '@tanstack/react-start';
2+
import { wrappedGlobalRequestMiddleware, wrappedGlobalFunctionMiddleware } from './middleware';
3+
4+
export const startInstance = createStart(() => {
5+
return {
6+
requestMiddleware: [wrappedGlobalRequestMiddleware],
7+
functionMiddleware: [wrappedGlobalFunctionMiddleware],
8+
};
9+
});
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
test('Sends spans for multiple middlewares and verifies they are siblings under the same parent span', async ({
5+
page,
6+
}) => {
7+
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
8+
return (
9+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
10+
!!transactionEvent?.transaction?.startsWith('GET /_serverFn')
11+
);
12+
});
13+
14+
await page.goto('/test-middleware');
15+
await expect(page.locator('#server-fn-middleware-btn')).toBeVisible();
16+
await page.locator('#server-fn-middleware-btn').click();
17+
18+
const transactionEvent = await transactionEventPromise;
19+
20+
expect(Array.isArray(transactionEvent?.spans)).toBe(true);
21+
22+
// Find both middleware spans
23+
const serverFnMiddlewareSpan = transactionEvent?.spans?.find(
24+
(span: { description?: string; origin?: string }) =>
25+
span.description === 'serverFnMiddleware' && span.origin === 'manual.middleware.tanstackstart',
26+
);
27+
const globalFunctionMiddlewareSpan = transactionEvent?.spans?.find(
28+
(span: { description?: string; origin?: string }) =>
29+
span.description === 'globalFunctionMiddleware' && span.origin === 'manual.middleware.tanstackstart',
30+
);
31+
32+
// Verify both middleware spans exist with expected properties
33+
expect(serverFnMiddlewareSpan).toEqual(
34+
expect.objectContaining({
35+
description: 'serverFnMiddleware',
36+
op: 'middleware.tanstackstart',
37+
origin: 'manual.middleware.tanstackstart',
38+
status: 'ok',
39+
}),
40+
);
41+
expect(globalFunctionMiddlewareSpan).toEqual(
42+
expect.objectContaining({
43+
description: 'globalFunctionMiddleware',
44+
op: 'middleware.tanstackstart',
45+
origin: 'manual.middleware.tanstackstart',
46+
status: 'ok',
47+
}),
48+
);
49+
50+
// Both middleware spans should be siblings under the same parent
51+
expect(serverFnMiddlewareSpan?.parent_span_id).toBe(globalFunctionMiddlewareSpan?.parent_span_id);
52+
});
53+
54+
test('Sends spans for global function middleware', async ({ page }) => {
55+
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
56+
return (
57+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
58+
!!transactionEvent?.transaction?.startsWith('GET /_serverFn')
59+
);
60+
});
61+
62+
await page.goto('/test-middleware');
63+
await expect(page.locator('#server-fn-global-only-btn')).toBeVisible();
64+
await page.locator('#server-fn-global-only-btn').click();
65+
66+
const transactionEvent = await transactionEventPromise;
67+
68+
expect(Array.isArray(transactionEvent?.spans)).toBe(true);
69+
70+
// Check for the global function middleware span
71+
expect(transactionEvent?.spans).toEqual(
72+
expect.arrayContaining([
73+
expect.objectContaining({
74+
description: 'globalFunctionMiddleware',
75+
op: 'middleware.tanstackstart',
76+
origin: 'manual.middleware.tanstackstart',
77+
status: 'ok',
78+
}),
79+
]),
80+
);
81+
});
82+
83+
test('Sends spans for global request middleware', async ({ page }) => {
84+
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
85+
return (
86+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
87+
transactionEvent?.transaction === 'GET /test-middleware'
88+
);
89+
});
90+
91+
await page.goto('/test-middleware');
92+
93+
const transactionEvent = await transactionEventPromise;
94+
95+
expect(Array.isArray(transactionEvent?.spans)).toBe(true);
96+
97+
// Check for the global request middleware span
98+
expect(transactionEvent?.spans).toEqual(
99+
expect.arrayContaining([
100+
expect.objectContaining({
101+
description: 'globalRequestMiddleware',
102+
op: 'middleware.tanstackstart',
103+
origin: 'manual.middleware.tanstackstart',
104+
status: 'ok',
105+
}),
106+
]),
107+
);
108+
});
109+
110+
test('Sends spans for server route request middleware', async ({ page }) => {
111+
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
112+
return (
113+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
114+
transactionEvent?.transaction === 'GET /api/test-middleware'
115+
);
116+
});
117+
118+
await page.goto('/api/test-middleware');
119+
120+
const transactionEvent = await transactionEventPromise;
121+
122+
expect(Array.isArray(transactionEvent?.spans)).toBe(true);
123+
124+
// Check for the server route request middleware span
125+
expect(transactionEvent?.spans).toEqual(
126+
expect.arrayContaining([
127+
expect.objectContaining({
128+
description: 'serverRouteRequestMiddleware',
129+
op: 'middleware.tanstackstart',
130+
origin: 'manual.middleware.tanstackstart',
131+
status: 'ok',
132+
}),
133+
]),
134+
);
135+
});
136+
137+
test('Sends span for middleware that returns early without calling next()', async ({ page }) => {
138+
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
139+
return (
140+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
141+
!!transactionEvent?.transaction?.startsWith('GET /_serverFn')
142+
);
143+
});
144+
145+
await page.goto('/test-middleware');
146+
await expect(page.locator('#server-fn-early-return-btn')).toBeVisible();
147+
await page.locator('#server-fn-early-return-btn').click();
148+
149+
const transactionEvent = await transactionEventPromise;
150+
151+
expect(Array.isArray(transactionEvent?.spans)).toBe(true);
152+
153+
// Check for the early return middleware span
154+
expect(transactionEvent?.spans).toEqual(
155+
expect.arrayContaining([
156+
expect.objectContaining({
157+
description: 'earlyReturnMiddleware',
158+
op: 'middleware.tanstackstart',
159+
origin: 'manual.middleware.tanstackstart',
160+
status: 'ok',
161+
}),
162+
]),
163+
);
164+
});
165+
166+
test('Sends span for middleware that throws an error', async ({ page }) => {
167+
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
168+
return (
169+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
170+
!!transactionEvent?.transaction?.startsWith('GET /_serverFn')
171+
);
172+
});
173+
174+
await page.goto('/test-middleware');
175+
await expect(page.locator('#server-fn-error-btn')).toBeVisible();
176+
await page.locator('#server-fn-error-btn').click();
177+
178+
const transactionEvent = await transactionEventPromise;
179+
180+
expect(Array.isArray(transactionEvent?.spans)).toBe(true);
181+
182+
// Check for the error middleware span
183+
expect(transactionEvent?.spans).toEqual(
184+
expect.arrayContaining([
185+
expect.objectContaining({
186+
description: 'errorMiddleware',
187+
op: 'middleware.tanstackstart',
188+
origin: 'manual.middleware.tanstackstart',
189+
}),
190+
]),
191+
);
192+
});

0 commit comments

Comments
 (0)