Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(tanstackstart-react): Add global sentry exception middlewares#19330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
cd3f74e71eed6ff77a6f59c6fa18bbcd83ec4e49e368ce37d6ead206554f305d80c4451afee99ab0c88c9822576aa06362398f1d8271a7881d8b310a3e26da3061ef977d0188ec4cffad4ca904File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { createFileRoute } from '@tanstack/react-router'; | ||
| import { flush } from '@sentry/tanstackstart-react'; | ||
| export const Route = createFileRoute('/api/flush')({ | ||
| server: { | ||
| handlers: { | ||
| GET: async () => { | ||
| await flush(); | ||
| return new Response('ok'); | ||
| }, | ||
| }, | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { createFileRoute } from '@tanstack/react-router'; | ||
| export const Route = createFileRoute('/ssr-error')({ | ||
| loader: () => { | ||
| throw new Error('Sentry SSR Test Error'); | ||
| }, | ||
| component: () => <div>SSR Error Page</div>, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,11 @@ | ||
| import { sentryGlobalFunctionMiddleware, sentryGlobalRequestMiddleware } from '@sentry/tanstackstart-react'; | ||
| import { createStart } from '@tanstack/react-start'; | ||
| // NOTE: These are NOT wrapped - auto-instrumentation via the Vite plugin will wrap them | ||
| import { globalRequestMiddleware, globalFunctionMiddleware } from './middleware'; | ||
| import { globalFunctionMiddleware, globalRequestMiddleware } from './middleware'; | ||
| export const startInstance = createStart(() => { | ||
| return { | ||
| requestMiddleware: [globalRequestMiddleware], | ||
| functionMiddleware: [globalFunctionMiddleware], | ||
| requestMiddleware: [sentryGlobalRequestMiddleware, globalRequestMiddleware], | ||
| functionMiddleware: [sentryGlobalFunctionMiddleware, globalFunctionMiddleware], | ||
| }; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| export type TanStackMiddlewareBase = { | ||
| options?: { server?: (...args: unknown[]) => unknown }; | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| '~types': any; | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| options: { server?: (...args: any[]) => any }; | ||
MemberAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typescript kept complaining in my sample app that the types of the new sentry middlewares clash with what is expected from tanstack (it was still running fine thought), had to change it to this to fix that nicohrubec marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| }; | ||
| export type MiddlewareWrapperOptions = { | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { addNonEnumerableProperty, captureException } from '@sentry/core'; | ||
| import type { TanStackMiddlewareBase } from '../common/types'; | ||
| import { SENTRY_INTERNAL } from './middleware'; | ||
| function createSentryMiddlewareHandler(mechanismType: string) { | ||
| return async function sentryMiddlewareHandler({ next }: { next: () => Promise<unknown> }): Promise<unknown> { | ||
| try { | ||
| return await next(); | ||
| } catch (e) { | ||
| captureException(e, { | ||
| mechanism: { type: mechanismType, handled: false }, | ||
| }); | ||
| throw e; | ||
| } | ||
| }; | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| /** | ||
| * Global request middleware that captures errors from API route requests. | ||
| * Should be added as the first entry in the `requestMiddleware` array of `createStart()`. | ||
| */ | ||
| export const sentryGlobalRequestMiddleware: TanStackMiddlewareBase = { | ||
| '~types': undefined, | ||
| options: { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| server: createSentryMiddlewareHandler('auto.middleware.tanstackstart.request') as (...args: any[]) => any, | ||
| }, | ||
| }; | ||
| /** | ||
| * Global function middleware that captures errors from server function invocations. | ||
| * Should be added as the first entry in the `functionMiddleware` array of `createStart()`. | ||
| */ | ||
| export const sentryGlobalFunctionMiddleware: TanStackMiddlewareBase = { | ||
| '~types': undefined, | ||
| options: { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| server: createSentryMiddlewareHandler('auto.middleware.tanstackstart.server_function') as (...args: any[]) => any, | ||
| }, | ||
| }; | ||
| // Mark as internal so the Vite auto-instrumentation plugin skips these middleware | ||
| addNonEnumerableProperty(sentryGlobalRequestMiddleware, SENTRY_INTERNAL, true); | ||
| addNonEnumerableProperty(sentryGlobalFunctionMiddleware, SENTRY_INTERNAL, true); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import { afterEach, describe, expect, it, vi } from 'vitest'; | ||
| const captureExceptionSpy = vi.fn(); | ||
| vi.mock('@sentry/core', async importOriginal => { | ||
| const original = await importOriginal(); | ||
| return { | ||
| ...original, | ||
| captureException: (...args: unknown[]) => captureExceptionSpy(...args), | ||
| }; | ||
| }); | ||
| // Import after mocks are set up | ||
| const { sentryGlobalRequestMiddleware, sentryGlobalFunctionMiddleware } = | ||
| await import('../../src/server/globalMiddleware'); | ||
| describe('sentryGlobalRequestMiddleware', () => { | ||
| afterEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
| it('captures error with correct mechanism when next() throws', async () => { | ||
| const error = new Error('test error'); | ||
| const next = vi.fn().mockRejectedValue(error); | ||
| const serverFn = sentryGlobalRequestMiddleware.options.server!; | ||
| await expect(serverFn({ next })).rejects.toThrow('test error'); | ||
| expect(captureExceptionSpy).toHaveBeenCalledWith(error, { | ||
| mechanism: { type: 'auto.middleware.tanstackstart.request', handled: false }, | ||
| }); | ||
| }); | ||
| it('does not capture error when next() succeeds', async () => { | ||
| const next = vi.fn().mockResolvedValue('success'); | ||
| const serverFn = sentryGlobalRequestMiddleware.options.server!; | ||
| const result = await serverFn({ next }); | ||
| expect(result).toBe('success'); | ||
| expect(captureExceptionSpy).not.toHaveBeenCalled(); | ||
| }); | ||
| it('has __SENTRY_INTERNAL__ flag set', () => { | ||
| expect((sentryGlobalRequestMiddleware as unknown as Record<string, unknown>)['__SENTRY_INTERNAL__']).toBe(true); | ||
| }); | ||
| }); | ||
| describe('sentryGlobalFunctionMiddleware', () => { | ||
| afterEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
| it('captures error with correct mechanism when next() throws', async () => { | ||
| const error = new Error('test error'); | ||
| const next = vi.fn().mockRejectedValue(error); | ||
| const serverFn = sentryGlobalFunctionMiddleware.options.server!; | ||
| await expect(serverFn({ next })).rejects.toThrow('test error'); | ||
| expect(captureExceptionSpy).toHaveBeenCalledWith(error, { | ||
| mechanism: { type: 'auto.middleware.tanstackstart.server_function', handled: false }, | ||
| }); | ||
| }); | ||
| it('has __SENTRY_INTERNAL__ flag set', () => { | ||
| expect((sentryGlobalFunctionMiddleware as unknown as Record<string, unknown>)['__SENTRY_INTERNAL__']).toBe(true); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.