Skip to content

Conversation

@nicohrubec
Copy link
Member

@nicohrubec nicohrubec commented Jan 2, 2026

This PR adds a middleware wrapper to the tanstackstart SDK that allows users to add tracing to their application middleware. Eventually we will want to patch this automatically, but that is a bit tricky since it requires build-time magic. This API provides a manual alternative for now and can later still act as a fallback for cases where auto-instrumentation doesn't work.

How it works
The wrapper patches the middleware options.server function that gets executed whenever a middleware is run. Each middleware invocation creates a span with:

  • op: middleware.tanstackstart
  • origin: manual.middleware.tanstackstart
  • name: The instrumentation automatically assigns the middleware name based on the variable name assigned to the middleware.

At first I had the issue that if multiple middlewares were used they would be nested (i.e. first middleware is parent of second etc.). This is because the middlewares call next() to move down the middleware chain, so trivially starting a span for the middleware execution would actually create a span that would last for the current middleware and any middlewares that come after in the middleware chain. I fixed that by also proxying next(), where I end the middleware span and then also reattach the middleware spans to the parent request span instead of the previous middleware span.

Usage

import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';

  const [wrappedAuth, wrappedLogging] = wrapMiddlewaresWithSentry({
    authMiddleware,
    loggingMiddleware,
  });

Tests

Added E2E tests for:

  • if multiple middlewares are executed we get spans for both and they are sibling spans (i.e. children of the same parent)
  • global request middleware
  • global function middleware
  • request middleware

Screenshots from sample app

Using two global request middlewares:

Screenshot 2026-01-05 at 16 19 03

Closes #18666

@nicohrubec nicohrubec marked this pull request as ready for review January 5, 2026 16:07
function getNextProxy<T extends (...args: unknown[]) => unknown>(next: T, span: Span, prevSpan: Span | undefined): T {
return new Proxy(next, {
apply: (originalNext, thisArgNext, argsNext) => {
span.end();

This comment was marked as outdated.

}

return originalServer.apply(thisArgServer, argsServer);
});
Copy link

Choose a reason for hiding this comment

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

Span never ends when middleware throws or skips next()

The span created by startSpanManual is only ended inside getNextProxy when next() is called. If the middleware throws an error (e.g., authentication failure) or returns early without calling next(), the span will never be ended. This causes orphaned spans and potential memory leaks. The trpcMiddleware implementation demonstrates the correct pattern: wrapping the middleware execution in a try-catch and calling span.end() in both success and error paths. Additionally, per the review rules, captureException with a proper mechanism should be called when an error occurs.

Fix in Cursor Fix in Web

apply: (originalServer, thisArgServer, argsServer) => {
const prevSpan = getActiveSpan();

return startSpanManual(getMiddlewareSpanOptions(options.name), (span: Span) => {
Copy link
Member

Choose a reason for hiding this comment

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

Just a general question: in wrapMiddlewareListWithSentry the name is gotten from the middleware itself. Can something like this be done here as well? Because it also has access to the middleware.

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought about this as well but for that to work we need to pass it in as an object

* @param options - Options for the wrapper (currently only the name of the middleware)
* @returns The patched middleware
*/
export function wrapMiddlewareWithSentry<T extends TanStackMiddlewareBase>(
Copy link
Member

Choose a reason for hiding this comment

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

I am unsure about having two different APIs here. Is there any specific reason for that? Maybe I'm missing something.

Using wrapMiddlewareWithSentry is very verbose as you also have to write the name, so it might be preferred to just use the other function, which accepts the middleware with its name.

And the naming is quite similar and I think it could confuse users about what to use and if there are differences.

  1. I would either have one function wrapMiddlewareWithSentry which accepts different parameters (either one or more middlewares)
  2. or just have wrapMiddlewareWithSentry with passing the middleware(s) within an object.

Copy link
Member Author

@nicohrubec nicohrubec Jan 8, 2026

Choose a reason for hiding this comment

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

Sure I think that's fair, having one API should be much easier for users. I'll change it to one wrapMiddlewaresWithSentry API that accepts an object where people can pass as many middlewares as they want (1 or multiple). Object so we can grab the variable name instead of users needing to pass in names for all the middlewares they want to wrap

Copy link
Member Author

Choose a reason for hiding this comment

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

updated

@nicohrubec nicohrubec changed the title feat(tanstackstart-react): Add wrappers for manual middleware instrumentation feat(tanstackstart-react): Add wrappers for manual instrumentation of servers-side middlewares Jan 8, 2026
@nicohrubec nicohrubec requested a review from s1gr1d January 8, 2026 06:57
@github-actions
Copy link
Contributor

github-actions bot commented Jan 8, 2026

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 9,033 - 8,987 +1%
GET With Sentry 1,769 20% 1,675 +6%
GET With Sentry (error only) 6,111 68% 6,171 -1%
POST Baseline 1,191 - 1,207 -1%
POST With Sentry 595 50% 576 +3%
POST With Sentry (error only) 1,064 89% 1,036 +3%
MYSQL Baseline 3,264 - 3,299 -1%
MYSQL With Sentry 485 15% 423 +15%
MYSQL With Sentry (error only) 2,663 82% 2,700 -1%

View base workflow run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Manual middleware tracing

3 participants