-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(tanstackstart-react): Auto-instrument global middleware #18844
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nicohrubec
wants to merge
38
commits into
develop
Choose a base branch
from
nh/automatic-middleware-instrumentation
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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 6cc5784
align usage with solidstart
nicohrubec b4a0d83
Add placeholder to add plugins
nicohrubec b6ddbff
Add sentry vite plugin and enable source maps plugins automatically
nicohrubec 8c6f0fa
?
nicohrubec 207a96a
add unit tests
nicohrubec 53e9099
add vite wrapper to e2e tests
nicohrubec b9c51e9
simplify unit tests
nicohrubec 6eb1c8b
use buildTimeOptionsBase instead of defining my own type
nicohrubec 4dcdabf
Merge branch 'develop' into nh/tss-vite-config-wrapper
nicohrubec 9aaabd9
.
nicohrubec bfa238a
switch to sentry vite plugin
nicohrubec 086b9f6
add changelog entry and pass down all options
nicohrubec 05e08f1
clean
nicohrubec bc0acdd
always add sentry vite plugin and pass down disable option
nicohrubec 8e60de8
Merge branch 'develop' into nh/tss-vite-config-wrapper
nicohrubec 9bbd0b0
readability
nicohrubec 283a545
update tests
nicohrubec 1e736b4
Merge branch 'develop' into nh/tss-vite-config-wrapper
nicohrubec 841a311
bump bundler plugins
nicohrubec 9ad7a8d
update
nicohrubec 286f624
update bundler plugins fr this time
nicohrubec 505c92c
Revert "update bundler plugins fr this time"
nicohrubec 07d5e77
Merge branch 'develop' into nh/tss-vite-config-wrapper
nicohrubec 073e352
Merge branch 'develop' into nh/tss-vite-config-wrapper
nicohrubec 8ce5358
update sentry vite plugin
nicohrubec 76c746c
address some pr comments
nicohrubec bbf7be4
use post for config plugin
nicohrubec 63a6660
fix files to delete after upload settings
nicohrubec c99e1a1
Merge branch 'develop' into nh/tss-vite-config-wrapper
nicohrubec e2bf4b0
make global middleware auto-wrapping work
nicohrubec b7d35ad
update unit tests for sentryTanstackStart
nicohrubec c6d7624
do not transform files with manul middleware wrapping
nicohrubec 8142b20
clean
nicohrubec 463d7f0
clean
nicohrubec 1ed2308
.
nicohrubec 2706bc0
Add changelog entry
nicohrubec bdb9ac5
Merge branch 'develop' into nh/automatic-middleware-instrumentation
nicohrubec File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 4 additions & 3 deletions
7
dev-packages/e2e-tests/test-applications/tanstackstart-react/src/start.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
88
packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(', ')} }`; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export { sentryTanstackStart } from './sentryTanstackStart'; | ||
| export type { SentryTanstackStartOptions } from './sentryTanstackStart'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
137 changes: 137 additions & 0 deletions
137
packages/tanstackstart-react/test/vite/autoInstrumentMiddleware.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.