Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b877f51
feat(tanstackstart-react): Auto-instrument request middleware
nicohrubec Jan 27, 2026
40a7ba4
refactor
nicohrubec Jan 27, 2026
3660c72
update e2e tests
nicohrubec Jan 27, 2026
9a8026b
refactor
nicohrubec Jan 27, 2026
e985b3e
yarn fix
nicohrubec Jan 27, 2026
f7f4c22
refactor
nicohrubec Jan 27, 2026
092858f
isStartFile XOR isRouteFile
nicohrubec Jan 27, 2026
ce8dbc7
guard for double import
nicohrubec Jan 27, 2026
330d7f8
add double wrapping test
nicohrubec Jan 27, 2026
b262eaa
Revert "add double wrapping test"
nicohrubec Jan 27, 2026
14873c3
feat(tanstackstart-react): Auto-instrument server function middleware
nicohrubec Jan 27, 2026
a910011
yarn fix
nicohrubec Jan 27, 2026
c122987
Merge branch 'develop' into nh/automatic-function-middleware-instrume…
nicohrubec Jan 28, 2026
25e86bc
refactor + simplify
nicohrubec Jan 28, 2026
1b2c92f
Add changelog entry
nicohrubec Jan 28, 2026
b94322a
update changelog
nicohrubec Jan 30, 2026
e7f8e23
.
nicohrubec Jan 30, 2026
df42f71
Merge branch 'develop' into nh/automatic-function-middleware-instrume…
nicohrubec Jan 30, 2026
40ba4ec
feat(tanstackstart-react): Add file exclude to configure middleware a…
nicohrubec Jan 27, 2026
9912468
shouldSkipFile
nicohrubec Jan 28, 2026
fc0632a
.
nicohrubec Jan 28, 2026
478e1a2
remove enabled option
nicohrubec Jan 28, 2026
07f4f30
update tests
nicohrubec Jan 28, 2026
d41a450
Merge branch 'develop' into nh/ignore-files-from-auto-instrument
nicohrubec Jan 30, 2026
57da296
use path.extname
nicohrubec Jan 30, 2026
e806bea
only ts/tsx
nicohrubec Feb 2, 2026
d9421fc
.
nicohrubec Feb 2, 2026
529a1e6
.
nicohrubec Feb 2, 2026
bfc47ee
Add changelog
nicohrubec Feb 2, 2026
555c71e
Adjust size limit
nicohrubec Feb 2, 2026
1778822
fix import ordering
nicohrubec Feb 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
ignore: ['react/jsx-runtime'],
gzip: true,
limit: '44.5 KB',
limit: '44.6 KB',
},
// Vue SDK (ESM)
{
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@

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

- **feat(tanstackstart-react): Add `exclude` option for auto-instrumented middleware ([#19007](https://github.com/getsentry/sentry-javascript/pull/19007))**

The `autoInstrumentMiddleware` option in `sentryTanstackStart()` now accepts an object with an `exclude` property to skip specific files from automatic instrumentation. Patterns can be strings (matched as substrings) or regular expressions matched against the full file path.

```ts
sentryTanstackStart({
autoInstrumentMiddleware: {
exclude: ['/routes/admin/', /\.test\.ts$/],
},
});
```

- **feat(tanstackstart-react): Auto-instrument server function middleware ([#19001](https://github.com/getsentry/sentry-javascript/pull/19001))**

The `sentryTanstackStart` Vite plugin now automatically instruments middleware in `createServerFn().middleware([...])` calls. This captures performance data without requiring manual wrapping with `wrapMiddlewaresWithSentry()`.
Expand Down
31 changes: 26 additions & 5 deletions packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { stringMatchesSomePattern } from '@sentry/core';
import * as path from 'path';
import type { Plugin } from 'vite';

type AutoInstrumentMiddlewareOptions = {
enabled?: boolean;
Copy link
Member Author

@nicohrubec nicohrubec Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an unnecessary option since we just don't add this plugin if it's not enabled, so I removed it

debug?: boolean;
exclude?: Array<string | RegExp>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would make sense to add some defaults here. E.g. excluding the test files if this is a very common usecase

Copy link
Member Author

@nicohrubec nicohrubec Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll think about this some more and maybe do a follow up, for the test files for instance I think the better approach for us would be to add this to the skipped file extensions early return instead of using this option

};

type WrapResult = {
Expand Down Expand Up @@ -87,25 +89,44 @@ function applyWrap(
};
}

/**
* Checks if a file should be skipped from auto-instrumentation based on exclude patterns.
*/
export function shouldSkipFile(id: string, exclude: Array<string | RegExp> | 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}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: I think this should be rather debug.log, where debug comes from @sentry/core

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to be console.log since it happens during build not runtime

}
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/TSX file
const fileExtension = path.extname(id);
if (!['.ts', '.tsx'].includes(fileExtension)) {
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;
}

Expand Down
42 changes: 34 additions & 8 deletions packages/tanstackstart-react/src/vite/sentryTanstackStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | RegExp>;
};
}

/**
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
addSentryImport,
arrayToObjectShorthand,
makeAutoInstrumentMiddlewarePlugin,
shouldSkipFile,
wrapGlobalMiddleware,
wrapRouteMiddleware,
wrapServerFnMiddleware,
Expand All @@ -29,20 +30,13 @@ export const Route = createFileRoute('/foo')({
});
`;

it('does not instrument non-TS/JS files', () => {
it('does not instrument non-TS/TSX files', () => {
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
const result = plugin.transform(createStartFile, '/app/start.css');

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';";
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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 }');
Expand Down
35 changes: 33 additions & 2 deletions packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});
});
});
Loading