diff --git a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts index 07cf2a29e832..0500081f7cf7 100644 --- a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts +++ b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts @@ -1,8 +1,9 @@ +import { stringMatchesSomePattern } from '@sentry/core'; import type { Plugin } from 'vite'; type AutoInstrumentMiddlewareOptions = { - enabled?: boolean; debug?: boolean; + exclude?: Array; }; type WrapResult = { @@ -87,25 +88,43 @@ function applyWrap( }; } +/** + * Checks if a file should be skipped from auto-instrumentation based on exclude patterns. + */ +export function shouldSkipFile(id: string, exclude: Array | undefined, debug: boolean): boolean { + // file doesn't match exclude patterns, don't skip + if (!exclude || exclude.length === 0 || !stringMatchesSomePattern(id, exclude)) { + return false; + } + + // file matches exclude patterns, skip + if (debug) { + // eslint-disable-next-line no-console + console.log(`[Sentry] Skipping auto-instrumentation for excluded file: ${id}`); + } + return true; +} + /** * A Vite plugin that automatically instruments TanStack Start middlewares: * - `requestMiddleware` and `functionMiddleware` arrays in `createStart()` * - `middleware` arrays in `createFileRoute()` route definitions */ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddlewareOptions = {}): Plugin { - const { enabled = true, debug = false } = options; + const { debug = false, exclude } = options; return { name: 'sentry-tanstack-middleware-auto-instrument', enforce: 'pre', transform(code, id) { - if (!enabled) { + // Skip if not a TS/JS file + if (!/\.(ts|tsx|js|jsx|mjs|mts)$/.test(id)) { return null; } - // Skip if not a TS/JS file - if (!/\.(ts|tsx|js|jsx|mjs|mts)$/.test(id)) { + // Skip if file matches exclude patterns + if (shouldSkipFile(id, exclude, debug)) { return null; } diff --git a/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts b/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts index d14033ff052d..ef8f04b7d9f7 100644 --- a/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts +++ b/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts @@ -8,17 +8,34 @@ import { makeAddSentryVitePlugin, makeEnableSourceMapsVitePlugin } from './sourc */ export interface SentryTanstackStartOptions extends BuildTimeOptionsBase { /** - * If this flag is `true`, the Sentry plugins will automatically instrument TanStack Start middlewares. + * Configure automatic middleware instrumentation. * - * This wraps global middlewares (`requestMiddleware` and `functionMiddleware`) in `createStart()` with Sentry - * instrumentation to capture performance data. + * - Set to `false` to disable automatic middleware instrumentation entirely. + * - Set to `true` (default) to enable for all middleware files. + * - Set to an object with `exclude` to enable but exclude specific files. * - * Set to `false` to disable automatic middleware instrumentation if you prefer to wrap middlewares manually - * using `wrapMiddlewaresWithSentry`. + * The `exclude` option takes an array of strings or regular expressions matched + * against the full file path. String patterns match as substrings. * * @default true + * + * @example + * // Disable completely + * sentryTanstackStart({ autoInstrumentMiddleware: false }) + * + * @example + * // Enable with exclusions + * sentryTanstackStart({ + * autoInstrumentMiddleware: { + * exclude: ['/routes/admin/', /\.test\.ts$/], + * }, + * }) */ - autoInstrumentMiddleware?: boolean; + autoInstrumentMiddleware?: + | boolean + | { + exclude?: Array; + }; } /** @@ -54,8 +71,17 @@ export function sentryTanstackStart(options: SentryTanstackStartOptions = {}): P const plugins: Plugin[] = [...makeAddSentryVitePlugin(options)]; // middleware auto-instrumentation - if (options.autoInstrumentMiddleware !== false) { - plugins.push(makeAutoInstrumentMiddlewarePlugin({ enabled: true, debug: options.debug })); + const autoInstrumentConfig = options.autoInstrumentMiddleware; + const isDisabled = autoInstrumentConfig === false; + const excludePatterns = typeof autoInstrumentConfig === 'object' ? autoInstrumentConfig.exclude : undefined; + + if (!isDisabled) { + plugins.push( + makeAutoInstrumentMiddlewarePlugin({ + debug: options.debug, + exclude: excludePatterns, + }), + ); } // source maps diff --git a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts index 7a58fa3237e4..e94695ce7adc 100644 --- a/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts +++ b/packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts @@ -4,6 +4,7 @@ import { addSentryImport, arrayToObjectShorthand, makeAutoInstrumentMiddlewarePlugin, + shouldSkipFile, wrapGlobalMiddleware, wrapRouteMiddleware, wrapServerFnMiddleware, @@ -36,13 +37,6 @@ export const Route = createFileRoute('/foo')({ expect(result).toBeNull(); }); - it('does not instrument when enabled is false', () => { - const plugin = makeAutoInstrumentMiddlewarePlugin({ enabled: false }) as PluginWithTransform; - const result = plugin.transform(createStartFile, '/app/start.ts'); - - expect(result).toBeNull(); - }); - it('does not instrument files without createStart or createFileRoute', () => { const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform; const code = "export const foo = 'bar';"; @@ -97,6 +91,14 @@ createStart(() => ({ requestMiddleware: [getMiddleware()] })); consoleWarnSpy.mockRestore(); }); + + it('does not instrument files matching exclude patterns', () => { + const plugin = makeAutoInstrumentMiddlewarePlugin({ + exclude: ['/routes/admin/'], + }) as PluginWithTransform; + const result = plugin.transform(createStartFile, '/app/routes/admin/start.ts'); + expect(result).toBeNull(); + }); }); describe('wrapGlobalMiddleware', () => { @@ -445,6 +447,37 @@ describe('addSentryImport', () => { }); }); +describe('shouldSkipFile', () => { + it('returns false when exclude is undefined', () => { + expect(shouldSkipFile('/app/start.ts', undefined, false)).toBe(false); + }); + + it('returns false when exclude is empty array', () => { + expect(shouldSkipFile('/app/start.ts', [], false)).toBe(false); + }); + + it('returns false when file does not match any pattern', () => { + expect(shouldSkipFile('/app/start.ts', ['/admin/', /\.test\.ts$/], false)).toBe(false); + }); + + it('returns true when file matches string pattern', () => { + expect(shouldSkipFile('/app/routes/admin/start.ts', ['/admin/'], false)).toBe(true); + }); + + it('returns true when file matches regex pattern', () => { + expect(shouldSkipFile('/app/start.test.ts', [/\.test\.ts$/], false)).toBe(true); + }); + + it('logs debug message when skipping file', () => { + const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + shouldSkipFile('/app/routes/admin/start.ts', ['/admin/'], true); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining('Skipping auto-instrumentation for excluded file'), + ); + consoleLogSpy.mockRestore(); + }); +}); + describe('arrayToObjectShorthand', () => { it('converts single identifier', () => { expect(arrayToObjectShorthand('foo')).toBe('{ foo }'); diff --git a/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts b/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts index ef18da74d03a..09669019b22b 100644 --- a/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts +++ b/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts @@ -118,13 +118,44 @@ describe('sentryTanstackStart()', () => { it('passes correct options to makeAutoInstrumentMiddlewarePlugin', () => { sentryTanstackStart({ debug: true, sourcemaps: { disable: true } }); - expect(makeAutoInstrumentMiddlewarePlugin).toHaveBeenCalledWith({ enabled: true, debug: true }); + expect(makeAutoInstrumentMiddlewarePlugin).toHaveBeenCalledWith({ + debug: true, + exclude: undefined, + }); }); it('passes debug: undefined when not specified', () => { sentryTanstackStart({ sourcemaps: { disable: true } }); - expect(makeAutoInstrumentMiddlewarePlugin).toHaveBeenCalledWith({ enabled: true, debug: undefined }); + expect(makeAutoInstrumentMiddlewarePlugin).toHaveBeenCalledWith({ + debug: undefined, + exclude: undefined, + }); + }); + + it('passes exclude patterns when autoInstrumentMiddleware is an object', () => { + const excludePatterns = ['/routes/admin/', /\.test\.ts$/]; + sentryTanstackStart({ + autoInstrumentMiddleware: { exclude: excludePatterns }, + sourcemaps: { disable: true }, + }); + + expect(makeAutoInstrumentMiddlewarePlugin).toHaveBeenCalledWith({ + debug: undefined, + exclude: excludePatterns, + }); + }); + + it('passes exclude: undefined when autoInstrumentMiddleware is true', () => { + sentryTanstackStart({ + autoInstrumentMiddleware: true, + sourcemaps: { disable: true }, + }); + + expect(makeAutoInstrumentMiddlewarePlugin).toHaveBeenCalledWith({ + debug: undefined, + exclude: undefined, + }); }); }); });