Skip to content

Commit 766eb98

Browse files
authored
feat(tanstackstart-react): Add global sentry exception middlewares (#19330)
This PR adds `sentryGlobalRequestMiddleware` and `sentryGlobalFunctionMiddleware` that capture unhandled errors from all HTTP requests and server function invocations. Users add these as the first entries in the `requestMiddleware` / `functionMiddleware` arrays of `createStart()`. These internal middlewares get marked with a `__SENTRY_INTERNAL__`, so that they can be easily skipped in the vite plugin to exclude them from middleware auto-instrumentation. Originally we wanted to do this in the server-entry-point, but since there haven't been any updates on this front in months I propose this as an alternative solution for now. This is probably slightly worse UX but in my case better than having nothing in place. We could also think about auto-injecting this during the build, but maybe not worth the effort since this is a one-time setup step. **Limitations** Tanstack Start has three types of server-side errors that we care about. With these middlewares we can capture 2 of these (route, function exceptions). We cannot capture SSR exceptions like this, because the exceptions are serialized at a deeper layer and newer thrown. **Usage** ``` import { sentryGlobalFunctionMiddleware, sentryGlobalRequestMiddleware } from '@sentry/tanstackstart-react'; import { createStart } from '@tanstack/react-start'; export const startInstance = createStart(() => ({ requestMiddleware: [sentryGlobalRequestMiddleware, ...otherMiddleware], functionMiddleware: [sentryGlobalFunctionMiddleware, ...otherMiddleware], })); ``` **Tests** - Updated E2E tests to verify server side function/route errors are being captured - Added an E2E test to document that SSR exceptions are NOT being captured Closes #18283
1 parent 2dcb151 commit 766eb98

13 files changed

Lines changed: 214 additions & 17 deletions

File tree

CHANGELOG.md

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

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

7+
- **feat(tanstackstart-react): Add global sentry exception middlewares ([#19330](https://github.com/getsentry/sentry-javascript/pull/19330))**
8+
9+
The `sentryGlobalRequestMiddleware` and `sentryGlobalFunctionMiddleware` global middlewares capture unhandled exceptions thrown in TanStack Start API routes and server functions. Add them as the first entries in the `requestMiddleware` and `functionMiddleware` arrays of `createStart()`:
10+
11+
```ts
12+
import { createStart } from '@tanstack/react-start/server';
13+
import { sentryGlobalRequestMiddleware, sentryGlobalFunctionMiddleware } from '@sentry/tanstackstart-react/server';
14+
15+
export default createStart({
16+
requestMiddleware: [sentryGlobalRequestMiddleware, myRequestMiddleware],
17+
functionMiddleware: [sentryGlobalFunctionMiddleware, myFunctionMiddleware],
18+
});
19+
```
20+
721
### Important Changes
822

923
- fix(node-core): Reduce bundle size by removing apm-js-collab and requiring pino >= 9.10 ([#18631](https://github.com/getsentry/sentry-javascript/pull/18631))

dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.error.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
1-
import * as Sentry from '@sentry/tanstackstart-react';
21
import { createFileRoute } from '@tanstack/react-router';
32

43
export const Route = createFileRoute('/api/error')({
54
server: {
65
handlers: {
76
GET: async () => {
8-
try {
9-
throw new Error('Sentry API Route Test Error');
10-
} catch (error) {
11-
Sentry.captureException(error);
12-
throw error;
13-
}
7+
throw new Error('Sentry API Route Test Error');
148
},
159
},
1610
},
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 { flush } from '@sentry/tanstackstart-react';
3+
4+
export const Route = createFileRoute('/api/flush')({
5+
server: {
6+
handlers: {
7+
GET: async () => {
8+
await flush();
9+
return new Response('ok');
10+
},
11+
},
12+
},
13+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createFileRoute } from '@tanstack/react-router';
2+
3+
export const Route = createFileRoute('/ssr-error')({
4+
loader: () => {
5+
throw new Error('Sentry SSR Test Error');
6+
},
7+
component: () => <div>SSR Error Page</div>,
8+
});
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import { sentryGlobalFunctionMiddleware, sentryGlobalRequestMiddleware } from '@sentry/tanstackstart-react';
12
import { createStart } from '@tanstack/react-start';
23
// NOTE: These are NOT wrapped - auto-instrumentation via the Vite plugin will wrap them
3-
import { globalRequestMiddleware, globalFunctionMiddleware } from './middleware';
4+
import { globalFunctionMiddleware, globalRequestMiddleware } from './middleware';
45

56
export const startInstance = createStart(() => {
67
return {
7-
requestMiddleware: [globalRequestMiddleware],
8-
functionMiddleware: [globalFunctionMiddleware],
8+
requestMiddleware: [sentryGlobalRequestMiddleware, globalRequestMiddleware],
9+
functionMiddleware: [sentryGlobalFunctionMiddleware, globalFunctionMiddleware],
910
};
1011
});

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

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForError } from '@sentry-internal/test-utils';
2+
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
33

44
test('Sends client-side error to Sentry with auto-instrumentation', async ({ page }) => {
55
const errorEventPromise = waitForError('tanstackstart-react', errorEvent => {
@@ -51,17 +51,18 @@ test('Sends server-side function error to Sentry with auto-instrumentation', asy
5151
type: 'Error',
5252
value: 'Sentry Server Function Test Error',
5353
mechanism: {
54+
type: 'auto.middleware.tanstackstart.server_function',
5455
handled: false,
5556
},
5657
},
5758
],
5859
},
5960
});
6061

61-
expect(errorEvent.transaction).toBe('/');
62+
expect(errorEvent.transaction).toEqual(expect.stringContaining('GET /_serverFn/'));
6263
});
6364

64-
test('Sends API route error to Sentry if manually instrumented', async ({ page }) => {
65+
test('Sends API route error to Sentry with auto-instrumentation', async ({ page }) => {
6566
const errorEventPromise = waitForError('tanstackstart-react', errorEvent => {
6667
return errorEvent?.exception?.values?.[0]?.value === 'Sentry API Route Test Error';
6768
});
@@ -81,7 +82,8 @@ test('Sends API route error to Sentry if manually instrumented', async ({ page }
8182
type: 'Error',
8283
value: 'Sentry API Route Test Error',
8384
mechanism: {
84-
handled: true,
85+
type: 'auto.middleware.tanstackstart.request',
86+
handled: false,
8587
},
8688
},
8789
],
@@ -90,3 +92,28 @@ test('Sends API route error to Sentry if manually instrumented', async ({ page }
9092

9193
expect(errorEvent.transaction).toBe('GET /api/error');
9294
});
95+
96+
// the sentry global middleware does not capture errors from SSR loader errors since they are serialized before they reach the middleware layer
97+
// this test verifies that the error is in fact not sent to Sentry
98+
test('Does not send SSR loader error to Sentry', async ({ baseURL, page }) => {
99+
let errorEventOccurred = false;
100+
101+
waitForError('tanstackstart-react', event => {
102+
if (!event.type && event.exception?.values?.[0]?.value === 'Sentry SSR Test Error') {
103+
errorEventOccurred = true;
104+
}
105+
return event?.transaction === 'GET /ssr-error';
106+
});
107+
108+
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
109+
return transactionEvent?.transaction === 'GET /ssr-error';
110+
});
111+
112+
await page.goto('/ssr-error');
113+
114+
await transactionEventPromise;
115+
116+
await (await fetch(`${baseURL}/api/flush`)).text();
117+
118+
expect(errorEventOccurred).toBe(false);
119+
});

packages/tanstackstart-react/src/client/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,15 @@ export { init } from './sdk';
1414
export function wrapMiddlewaresWithSentry<T extends TanStackMiddlewareBase>(middlewares: Record<string, T>): T[] {
1515
return Object.values(middlewares);
1616
}
17+
18+
/**
19+
* No-op stub for client-side builds.
20+
* The actual implementation is server-only, but this stub is needed to prevent rendering errors.
21+
*/
22+
export const sentryGlobalRequestMiddleware: TanStackMiddlewareBase = { '~types': undefined, options: {} };
23+
24+
/**
25+
* No-op stub for client-side builds.
26+
* The actual implementation is server-only, but this stub is needed to prevent rendering errors.
27+
*/
28+
export const sentryGlobalFunctionMiddleware: TanStackMiddlewareBase = { '~types': undefined, options: {} };

packages/tanstackstart-react/src/common/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
export type TanStackMiddlewareBase = {
2-
options?: { server?: (...args: unknown[]) => unknown };
2+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3+
'~types': any;
4+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5+
options: { server?: (...args: any[]) => any };
36
};
47

58
export type MiddlewareWrapperOptions = {

packages/tanstackstart-react/src/index.types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ export declare const statsigIntegration: typeof clientSdk.statsigIntegration;
3737
export declare const unleashIntegration: typeof clientSdk.unleashIntegration;
3838

3939
export declare const wrapMiddlewaresWithSentry: typeof serverSdk.wrapMiddlewaresWithSentry;
40+
41+
export declare const sentryGlobalRequestMiddleware: typeof serverSdk.sentryGlobalRequestMiddleware;
42+
export declare const sentryGlobalFunctionMiddleware: typeof serverSdk.sentryGlobalFunctionMiddleware;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { addNonEnumerableProperty, captureException } from '@sentry/core';
2+
import type { TanStackMiddlewareBase } from '../common/types';
3+
import { SENTRY_INTERNAL } from './middleware';
4+
5+
function createSentryMiddlewareHandler(mechanismType: string) {
6+
return async function sentryMiddlewareHandler({ next }: { next: () => Promise<unknown> }): Promise<unknown> {
7+
try {
8+
return await next();
9+
} catch (e) {
10+
captureException(e, {
11+
mechanism: { type: mechanismType, handled: false },
12+
});
13+
throw e;
14+
}
15+
};
16+
}
17+
18+
/**
19+
* Global request middleware that captures errors from API route requests.
20+
* Should be added as the first entry in the `requestMiddleware` array of `createStart()`.
21+
*/
22+
export const sentryGlobalRequestMiddleware: TanStackMiddlewareBase = {
23+
'~types': undefined,
24+
25+
options: {
26+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
27+
server: createSentryMiddlewareHandler('auto.middleware.tanstackstart.request') as (...args: any[]) => any,
28+
},
29+
};
30+
31+
/**
32+
* Global function middleware that captures errors from server function invocations.
33+
* Should be added as the first entry in the `functionMiddleware` array of `createStart()`.
34+
*/
35+
export const sentryGlobalFunctionMiddleware: TanStackMiddlewareBase = {
36+
'~types': undefined,
37+
38+
options: {
39+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
40+
server: createSentryMiddlewareHandler('auto.middleware.tanstackstart.server_function') as (...args: any[]) => any,
41+
},
42+
};
43+
44+
// Mark as internal so the Vite auto-instrumentation plugin skips these middleware
45+
addNonEnumerableProperty(sentryGlobalRequestMiddleware, SENTRY_INTERNAL, true);
46+
addNonEnumerableProperty(sentryGlobalFunctionMiddleware, SENTRY_INTERNAL, true);

0 commit comments

Comments
 (0)