Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
29 changes: 24 additions & 5 deletions packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { stringMatchesSomePattern } from '@sentry/core';
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>;
};

type WrapResult = {
Expand Down Expand Up @@ -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<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

}
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)) {
Copy link
Member

Choose a reason for hiding this comment

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

l: This was here before - still, maybe it makes more sense to have a path.extname(id) instead and check against this, it might be faster than regex or simply an endsWith (might be more effort, since this one only takes a string)

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;
Copy link

Choose a reason for hiding this comment

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

Bug: The check typeof autoInstrumentConfig === 'object' is insufficient because typeof null is also 'object', which can cause a crash if autoInstrumentConfig is null.
Severity: MEDIUM

Suggested Fix

Update the conditional check to explicitly handle the null case by adding && autoInstrumentConfig !== null. The corrected line should be: const excludePatterns = typeof autoInstrumentConfig === 'object' && autoInstrumentConfig !== null ? autoInstrumentConfig.exclude : undefined;.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/tanstackstart-react/src/vite/sentryTanstackStart.ts#L76

Potential issue: The code checks if `autoInstrumentConfig` is an object using `typeof
autoInstrumentConfig === 'object'`. However, in JavaScript, `typeof null` also evaluates
to `'object'`. If a user provides `null` for the `autoInstrumentMiddleware` option, for
instance through a dynamic JavaScript configuration, this check will pass. The
subsequent attempt to access `autoInstrumentConfig.exclude` will result in a `TypeError:
Cannot read property 'exclude' of null`, causing the Vite build process to crash. This
pattern is inconsistent with other parts of the codebase, which explicitly check for
`null` in similar situations.

Did we get this right? 👍 / 👎 to inform future reviews.


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 Down Expand Up @@ -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';";
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();
});
Copy link

Choose a reason for hiding this comment

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

Missing integration or E2E tests for feat PR

Low Severity · Bugbot Rules

This feat PR adds the file exclude functionality for middleware auto-instrumentation but only includes unit tests. Per the review rules: "When reviewing a feat PR, check if the PR includes at least one integration or E2E test. If neither of the two are present, add a comment, recommending to add one." The existing E2E test suite at dev-packages/e2e-tests/test-applications/tanstackstart-react/ could be extended to verify the exclude patterns work correctly in a real build scenario.

Additional Locations (1)

Fix in Cursor Fix in Web

});

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