Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(tanstackstart-react): Auto-instrument request middleware#18989
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
b877f5140a7ba43660c729a8026be985b3ef7f4c22092858fce8dbc7330d7f8b262eaaFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -5,9 +5,58 @@ type AutoInstrumentMiddlewareOptions = { | ||
| debug?: boolean; | ||
| }; | ||
| type WrapResult = { | ||
| code: string; | ||
| didWrap: boolean; | ||
| skipped: string[]; | ||
| }; | ||
| /** | ||
| * Core function that wraps middleware arrays matching the given regex. | ||
| */ | ||
| function wrapMiddlewareArrays(code: string, id: string, debug: boolean, regex: RegExp): WrapResult { | ||
| const skipped: string[] = []; | ||
| let didWrap = false; | ||
| const transformed = code.replace(regex, (match: string, key: string, contents: string) => { | ||
| const objContents = arrayToObjectShorthand(contents); | ||
| if (objContents) { | ||
| didWrap = 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 | ||
| // Skip if we matched whitespace only | ||
| if (contents.trim()) { | ||
| skipped.push(key); | ||
| } | ||
| return match; | ||
| }); | ||
| return { code: transformed, didWrap, skipped }; | ||
| } | ||
| /** | ||
| * Wraps global middleware arrays (requestMiddleware, functionMiddleware) in createStart() files. | ||
| */ | ||
| export function wrapGlobalMiddleware(code: string, id: string, debug: boolean): WrapResult { | ||
| return wrapMiddlewareArrays(code, id, debug, /(requestMiddleware|functionMiddleware)\s*:\s*\[([^\]]*)\]/g); | ||
| } | ||
| /** | ||
| * Wraps route middleware arrays in createFileRoute() files. | ||
| */ | ||
| export function wrapRouteMiddleware(code: string, id: string, debug: boolean): WrapResult { | ||
| return wrapMiddlewareArrays(code, id, debug, /(middleware)\s*:\s*\[([^\]]*)\]/g); | ||
| } | ||
| /** | ||
| * A Vite plugin that automatically instruments TanStack Start middlewares | ||
| * by wrapping `requestMiddleware` and `functionMiddleware` arrays in `createStart()`. | ||
| * 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; | ||
| @@ -26,9 +75,11 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle | ||
| return null; | ||
| } | ||
| // Only wrap requestMiddleware and functionMiddleware in createStart() | ||
| // createStart() should always be in a file named start.ts | ||
| if (!id.includes('start') || !code.includes('createStart(')) { | ||
| // Detect file types that should be instrumented | ||
| const isStartFile = id.includes('start') && code.includes('createStart('); | ||
| const isRouteFile = code.includes('createFileRoute(') && /middleware\s*:\s*\[/.test(code); | ||
| if (!isStartFile && !isRouteFile) { | ||
| return null; | ||
| } | ||
| @@ -41,26 +92,26 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle | ||
| 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 | ||
| // Skip if we matched whitespace only | ||
| if (contents.trim()) { | ||
| skippedMiddlewares.push(key); | ||
| } | ||
| return match; | ||
| }, | ||
| ); | ||
| switch (true) { | ||
| // global middleware | ||
| case isStartFile: { | ||
| const result = wrapGlobalMiddleware(transformed, id, debug); | ||
| transformed = result.code; | ||
| needsImport = needsImport || result.didWrap; | ||
| skippedMiddlewares.push(...result.skipped); | ||
| break; | ||
| } | ||
| // route middleware | ||
| case isRouteFile: { | ||
| const result = wrapRouteMiddleware(transformed, id, debug); | ||
| transformed = result.code; | ||
| needsImport = needsImport || result.didWrap; | ||
| skippedMiddlewares.push(...result.skipped); | ||
| break; | ||
| } | ||
| default: | ||
| break; | ||
| } | ||
| // Warn about middlewares that couldn't be auto-wrapped | ||
| if (skippedMiddlewares.length > 0) { | ||
| @@ -76,17 +127,7 @@ export function makeAutoInstrumentMiddlewarePlugin(options: AutoInstrumentMiddle | ||
| 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; | ||
| } | ||
| transformed = addSentryImport(transformed); | ||
| return { code: transformed, map: null }; | ||
| }, | ||
| @@ -117,3 +158,26 @@ export function arrayToObjectShorthand(contents: string): string | null { | ||
| return `{ ${uniqueItems.join(', ')} }`; | ||
| } | ||
| /** | ||
| * Adds the wrapMiddlewaresWithSentry import to the code. | ||
| * Handles 'use client' and 'use server' directives by inserting the import after them. | ||
| */ | ||
| export function addSentryImport(code: string): string { | ||
nicohrubec marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| const sentryImport = "import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';\n"; | ||
| // Don't add the import if it already exists | ||
| if (code.includes(sentryImport.trimEnd())) { | ||
| return code; | ||
| } | ||
| // Check for 'use server' or 'use client' directives, these need to be before any imports | ||
| const directiveMatch = code.match(/^(['"])use (client|server)\1;?\s*\n?/); | ||
| if (!directiveMatch) { | ||
| return sentryImport + code; | ||
| } | ||
| const directive = directiveMatch[0]; | ||
| return directive + sentryImport + code.slice(directive.length); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.