Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
704a781
Add placeholder vite config wrapper
nicohrubec Jan 7, 2026
6cc5784
align usage with solidstart
nicohrubec Jan 7, 2026
b4a0d83
Add placeholder to add plugins
nicohrubec Jan 7, 2026
b6ddbff
Add sentry vite plugin and enable source maps plugins automatically
nicohrubec Jan 7, 2026
8c6f0fa
?
nicohrubec Jan 7, 2026
207a96a
add unit tests
nicohrubec Jan 7, 2026
53e9099
add vite wrapper to e2e tests
nicohrubec Jan 8, 2026
b9c51e9
simplify unit tests
nicohrubec Jan 8, 2026
6eb1c8b
use buildTimeOptionsBase instead of defining my own type
nicohrubec Jan 13, 2026
4dcdabf
Merge branch 'develop' into nh/tss-vite-config-wrapper
nicohrubec Jan 13, 2026
9aaabd9
.
nicohrubec Jan 13, 2026
bfa238a
switch to sentry vite plugin
nicohrubec Jan 14, 2026
086b9f6
add changelog entry and pass down all options
nicohrubec Jan 14, 2026
05e08f1
clean
nicohrubec Jan 14, 2026
bc0acdd
always add sentry vite plugin and pass down disable option
nicohrubec Jan 14, 2026
8e60de8
Merge branch 'develop' into nh/tss-vite-config-wrapper
nicohrubec Jan 14, 2026
9bbd0b0
readability
nicohrubec Jan 14, 2026
283a545
update tests
nicohrubec Jan 14, 2026
1e736b4
Merge branch 'develop' into nh/tss-vite-config-wrapper
nicohrubec Jan 14, 2026
841a311
bump bundler plugins
nicohrubec Jan 15, 2026
9ad7a8d
update
nicohrubec Jan 15, 2026
286f624
update bundler plugins fr this time
nicohrubec Jan 15, 2026
505c92c
Revert "update bundler plugins fr this time"
nicohrubec Jan 15, 2026
07d5e77
Merge branch 'develop' into nh/tss-vite-config-wrapper
nicohrubec Jan 15, 2026
073e352
Merge branch 'develop' into nh/tss-vite-config-wrapper
nicohrubec Jan 16, 2026
8ce5358
update sentry vite plugin
nicohrubec Jan 16, 2026
76c746c
address some pr comments
nicohrubec Jan 16, 2026
bbf7be4
use post for config plugin
nicohrubec Jan 16, 2026
63a6660
fix files to delete after upload settings
nicohrubec Jan 16, 2026
c99e1a1
Merge branch 'develop' into nh/tss-vite-config-wrapper
nicohrubec Jan 16, 2026
e2bf4b0
make global middleware auto-wrapping work
nicohrubec Jan 16, 2026
b7d35ad
update unit tests for sentryTanstackStart
nicohrubec Jan 16, 2026
c6d7624
do not transform files with manul middleware wrapping
nicohrubec Jan 16, 2026
8142b20
clean
nicohrubec Jan 16, 2026
463d7f0
clean
nicohrubec Jan 16, 2026
1ed2308
.
nicohrubec Jan 16, 2026
2706bc0
Add changelog entry
nicohrubec Jan 16, 2026
bdb9ac5
Merge branch 'develop' into nh/automatic-middleware-instrumentation
nicohrubec Jan 16, 2026
a39e88f
handle use directive edge case and improve tests
nicohrubec Jan 19, 2026
00fbbbd
warn users if stuff goes wrong
nicohrubec Jan 19, 2026
993ba3f
deduplicate middleware entries
nicohrubec Jan 19, 2026
aa7adc5
yarn fix
nicohrubec Jan 19, 2026
65a1a3e
early return
nicohrubec Jan 19, 2026
7137d00
remove duplicate test
nicohrubec Jan 19, 2026
b5ed7e2
formatting
nicohrubec Jan 19, 2026
b0c6045
be more precise about what files to instrument
nicohrubec Jan 19, 2026
e0c1428
Merge branch 'develop' into nh/automatic-middleware-instrumentation
nicohrubec Jan 19, 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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@

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

- **feat(tanstackstart-react): Auto-instrument global middleware in `sentryTanstackStart` Vite plugin ([#18884](https://github.com/getsentry/sentry-javascript/pull/18844))**

The `sentryTanstackStart` Vite plugin now automatically instruments `requestMiddleware` and `functionMiddleware` arrays in `createStart()`. This captures performance data without requiring manual wrapping.

Auto-instrumentation is enabled by default. To disable it:

```ts
// vite.config.ts
sentryTanstackStart({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: 'your-org',
project: 'your-project',
autoInstrumentMiddleware: false,
});
```

## 10.35.0

### Important Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { createMiddleware } from '@tanstack/react-start';
import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';

// Global request middleware - runs on every request
const globalRequestMiddleware = createMiddleware().server(async ({ next }) => {
// NOTE: This is exported unwrapped to test auto-instrumentation via the Vite plugin
export const globalRequestMiddleware = createMiddleware().server(async ({ next }) => {
console.log('Global request middleware executed');
return next();
});

// Global function middleware - runs on every server function
const globalFunctionMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => {
// NOTE: This is exported unwrapped to test auto-instrumentation via the Vite plugin
export const globalFunctionMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => {
console.log('Global function middleware executed');
return next();
});
Expand Down Expand Up @@ -37,17 +39,13 @@ const errorMiddleware = createMiddleware({ type: 'function' }).server(async () =
throw new Error('Middleware Error Test');
});

// Manually wrap middlewares with Sentry
// Manually wrap middlewares with Sentry (for middlewares that won't be auto-instrumented)
export const [
wrappedGlobalRequestMiddleware,
wrappedGlobalFunctionMiddleware,
wrappedServerFnMiddleware,
wrappedServerRouteRequestMiddleware,
wrappedEarlyReturnMiddleware,
wrappedErrorMiddleware,
] = wrapMiddlewaresWithSentry({
globalRequestMiddleware,
globalFunctionMiddleware,
serverFnMiddleware,
serverRouteRequestMiddleware,
earlyReturnMiddleware,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createStart } from '@tanstack/react-start';
import { wrappedGlobalRequestMiddleware, wrappedGlobalFunctionMiddleware } from './middleware';
// NOTE: These are NOT wrapped - auto-instrumentation via the Vite plugin will wrap them
import { globalRequestMiddleware, globalFunctionMiddleware } from './middleware';

export const startInstance = createStart(() => {
return {
requestMiddleware: [wrappedGlobalRequestMiddleware],
functionMiddleware: [wrappedGlobalFunctionMiddleware],
requestMiddleware: [globalRequestMiddleware],
functionMiddleware: [globalFunctionMiddleware],
};
});
117 changes: 117 additions & 0 deletions packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { Plugin } from 'vite';

type AutoInstrumentMiddlewareOptions = {
enabled?: boolean;
debug?: boolean;
};

/**
* A Vite plugin that automatically instruments TanStack Start middlewares
* by wrapping `requestMiddleware` and `functionMiddleware` arrays in `createStart()`.
*/
export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddlewareOptions = {}): Plugin {
const { enabled = true, debug = false } = options;

return {
name: 'sentry-tanstack-middleware-auto-instrument',
enforce: 'pre',

transform(code, id) {
if (!enabled) {
return null;
}

// Skip if not a TS/JS file
if (!/\.(ts|tsx|js|jsx|mjs|mts)$/.test(id)) {
return null;
}

// Only wrap requestMiddleware and functionMiddleware in createStart()
if (!code.includes('createStart(')) {
return null;
}

// Skip if the user already did some manual wrapping
if (code.includes('wrapMiddlewaresWithSentry')) {
return null;
}

let transformed = code;
let needsImport = false;
const skippedMiddlewares: string[] = [];

transformed = transformed.replace(
/(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g,
(match: string, key: string, contents: string) => {
const objContents = arrayToObjectShorthand(contents);
if (objContents) {
needsImport = true;
if (debug) {
// eslint-disable-next-line no-console
console.log(`[Sentry] Auto-wrapping ${key} in ${id}`);
}
return `${key}: wrapMiddlewaresWithSentry(${objContents})`;
}
// Track middlewares that couldn't be auto-wrapped
if (contents.trim()) {
Copy link
Member

Choose a reason for hiding this comment

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

Q: why do we trim the contents here? To make sure it's not an empty file? Just curious

skippedMiddlewares.push(key);
}
return match;
},
);

// Warn about middlewares that couldn't be auto-wrapped
if (skippedMiddlewares.length > 0) {
// eslint-disable-next-line no-console
console.warn(
`[Sentry] Could not auto-instrument ${skippedMiddlewares.join(' and ')} in ${id}. ` +
'To instrument these middlewares, use wrapMiddlewaresWithSentry() manually. ',
);
}

// We didn't wrap any middlewares, so we don't need to import the wrapMiddlewaresWithSentry function
if (!needsImport) {
return null;
}

const sentryImport = "import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\n";

// Check for 'use server' or 'use client' directives, these need to be before any imports
const directiveMatch = transformed.match(/^(['"])use (client|server)\1;?\s*\n?/);
if (directiveMatch) {
// Insert import after the directive
const directive = directiveMatch[0];
transformed = directive + sentryImport + transformed.slice(directive.length);
} else {
transformed = sentryImport + transformed;
}

return { code: transformed, map: null };
},
};
}

/**
* Convert array contents to object shorthand syntax.
* e.g., "foo, bar, baz" → "{ foo, bar, baz }"
*
* Returns null if contents contain non-identifier expressions (function calls, etc.)
* which cannot be converted to object shorthand.
*/
export function arrayToObjectShorthand(contents: string): string | null {
const items = contents
.split(',')
.map(s => s.trim())
.filter(Boolean);

// Only convert if all items are valid identifiers (no complex expressions)
const allIdentifiers = items.every(item => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(item));
if (!allIdentifiers || items.length === 0) {
return null;
}

// Deduplicate to avoid invalid syntax like { foo, foo }
const uniqueItems = [...new Set(items)];

return `{ ${uniqueItems.join(', ')} }`;
}
1 change: 1 addition & 0 deletions packages/tanstackstart-react/src/vite/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { sentryTanstackStart } from './sentryTanstackStart';
export type { SentryTanstackStartOptions } from './sentryTanstackStart';
30 changes: 28 additions & 2 deletions packages/tanstackstart-react/src/vite/sentryTanstackStart.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
import type { BuildTimeOptionsBase } from '@sentry/core';
import type { Plugin } from 'vite';
import { makeAutoInstrumentMiddlewarePlugin } from './autoInstrumentMiddleware';
import { makeAddSentryVitePlugin, makeEnableSourceMapsVitePlugin } from './sourceMaps';

/**
* Build-time options for the Sentry TanStack Start SDK.
*/
export interface SentryTanstackStartOptions extends BuildTimeOptionsBase {
/**
* If this flag is `true`, the Sentry plugins will automatically instrument TanStack Start middlewares.
*
* This wraps global middlewares (`requestMiddleware` and `functionMiddleware`) in `createStart()` with Sentry
* instrumentation to capture performance data.
*
* Set to `false` to disable automatic middleware instrumentation if you prefer to wrap middlewares manually
* using `wrapMiddlewaresWithSentry`.
*
* @default true
*/
autoInstrumentMiddleware?: boolean;
}

/**
* Vite plugins for the Sentry TanStack Start SDK.
*
Expand All @@ -26,14 +45,21 @@ import { makeAddSentryVitePlugin, makeEnableSourceMapsVitePlugin } from './sourc
* @param options - Options to configure the Sentry Vite plugins
* @returns An array of Vite plugins
*/
export function sentryTanstackStart(options: BuildTimeOptionsBase = {}): Plugin[] {
// Only add plugins in production builds
export function sentryTanstackStart(options: SentryTanstackStartOptions = {}): Plugin[] {
// only add plugins in production builds
if (process.env.NODE_ENV === 'development') {
return [];
}

const plugins: Plugin[] = [...makeAddSentryVitePlugin(options)];

// middleware auto-instrumentation
const autoInstrumentMiddleware = options.autoInstrumentMiddleware !== false;
if (autoInstrumentMiddleware) {
Comment on lines +57 to +58
Copy link
Member

Choose a reason for hiding this comment

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

This value is just used once and it's not too complicated to understand what the condition is without having this extra variable :)

Suggested change
const autoInstrumentMiddleware = options.autoInstrumentMiddleware !== false;
if (autoInstrumentMiddleware) {
if (options.autoInstrumentMiddleware !== false) {

plugins.push(makeAutoInstrumentMiddlewarePlugin({ enabled: true, debug: options.debug }));
}

// source maps
const sourceMapsDisabled = options.sourcemaps?.disable === true || options.sourcemaps?.disable === 'disable-upload';
if (!sourceMapsDisabled) {
plugins.push(...makeEnableSourceMapsVitePlugin(options));
Expand Down
Loading
Loading