Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 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
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
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,23 @@

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

- **feat(tanstackstart-react): Add `sentryTanstackStart` Vite plugin for source maps upload**
- **feat(tanstackstart-react): Auto-instrument global middleware in `sentryTanstackStart` Vite plugin**

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,
});
```

- **feat(tanstackstart-react): Add sentryTanstackStart vite plugin to manage automatic source map uploads**

You can now configure source maps upload for TanStack Start using the `sentryTanstackStart` Vite plugin:

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],
};
});
88 changes: 88 additions & 0 deletions packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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;

transformed = transformed.replace(
/(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g,
(match, key, contents) => {
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})`;
}
return match;
},
);

if (needsImport) {
transformed = `import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\n${transformed}`;
return { code: transformed, map: null };
}

return 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;
}

return `{ ${items.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) {
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import type { Plugin } from 'vite';
import { describe, expect, it } from 'vitest';
import { arrayToObjectShorthand, makeAutoInstrumentMiddlewarePlugin } from '../../src/vite/autoInstrumentMiddleware';

type PluginWithTransform = Plugin & {
transform: (code: string, id: string) => { code: string; map: null } | null;
};

describe('makeAutoInstrumentMiddlewarePlugin', () => {
const createStartFile = `
import { createStart } from '@tanstack/react-start';
import { authMiddleware, loggingMiddleware } from './middleware';

export const startInstance = createStart(() => ({
requestMiddleware: [authMiddleware],
functionMiddleware: [loggingMiddleware],
}));
`;

it('instruments a file with createStart and middleware arrays', () => {
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
const result = plugin.transform(createStartFile, '/app/start.ts');

expect(result).not.toBeNull();
expect(result!.code).toContain("import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react'");
expect(result!.code).toContain('requestMiddleware: wrapMiddlewaresWithSentry({ authMiddleware })');
expect(result!.code).toContain('functionMiddleware: wrapMiddlewaresWithSentry({ loggingMiddleware })');
});

it('does not instrument files without createStart', () => {
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
const code = "export const foo = 'bar';";
const result = plugin.transform(code, '/app/other.ts');

expect(result).toBeNull();
});

it('does not instrument non-TS/JS 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('wraps single middleware entry correctly', () => {
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
const code = `
import { createStart } from '@tanstack/react-start';
createStart(() => ({ requestMiddleware: [singleMiddleware] }));
`;
const result = plugin.transform(code, '/app/start.ts');

expect(result!.code).toContain('requestMiddleware: wrapMiddlewaresWithSentry({ singleMiddleware })');
});

it('wraps multiple middleware entries correctly', () => {
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
const code = `
import { createStart } from '@tanstack/react-start';
createStart(() => ({ requestMiddleware: [a, b, c] }));
`;
const result = plugin.transform(code, '/app/start.ts');

expect(result!.code).toContain('requestMiddleware: wrapMiddlewaresWithSentry({ a, b, c })');
});

it('does not wrap empty middleware arrays', () => {
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
const code = `
import { createStart } from '@tanstack/react-start';
createStart(() => ({ requestMiddleware: [] }));
`;
const result = plugin.transform(code, '/app/start.ts');

expect(result).toBeNull();
});

it('does not wrap if middleware contains function calls', () => {
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
const code = `
import { createStart } from '@tanstack/react-start';
createStart(() => ({ requestMiddleware: [getMiddleware()] }));
`;
const result = plugin.transform(code, '/app/start.ts');

expect(result).toBeNull();
});

it('does not instrument files that already use wrapMiddlewaresWithSentry', () => {
const plugin = makeAutoInstrumentMiddlewarePlugin() as PluginWithTransform;
const code = `
import { createStart } from '@tanstack/react-start';
import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';
createStart(() => ({ requestMiddleware: wrapMiddlewaresWithSentry({ myMiddleware }) }));
`;
const result = plugin.transform(code, '/app/start.ts');

expect(result).toBeNull();
});
});

describe('arrayToObjectShorthand', () => {
it('converts single identifier', () => {
expect(arrayToObjectShorthand('foo')).toBe('{ foo }');
});

it('converts multiple identifiers', () => {
expect(arrayToObjectShorthand('foo, bar, baz')).toBe('{ foo, bar, baz }');
});

it('handles whitespace', () => {
expect(arrayToObjectShorthand(' foo , bar ')).toBe('{ foo, bar }');
});

it('returns null for empty string', () => {
expect(arrayToObjectShorthand('')).toBeNull();
});

it('returns null for function calls', () => {
expect(arrayToObjectShorthand('getMiddleware()')).toBeNull();
});

it('returns null for spread syntax', () => {
expect(arrayToObjectShorthand('...middlewares')).toBeNull();
});

it('returns null for mixed valid and invalid', () => {
expect(arrayToObjectShorthand('foo, bar(), baz')).toBeNull();
});
});
Loading
Loading