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

Copy link
Member

Choose a reason for hiding this comment

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

This could be a legit problem. Maybe you can test throwing an error in the middleware to make sure nothing unexpected happens here.

@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

}

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

Choose a reason for hiding this comment

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

This could be a legit problem. Maybe you can test throwing an error in the middleware to make sure nothing unexpected happens here.

Comment on lines +47 to +56
return startSpanManual(getMiddlewareSpanOptions(options.name), (span: Span) => {
// The server function receives { next, context, request } as first argument
// We need to proxy the `next` function inside that object
const middlewareArgs = argsServer[0] as { next?: (...args: unknown[]) => unknown } | undefined;
if (middlewareArgs && typeof middlewareArgs === 'object' && typeof middlewareArgs.next === 'function') {
middlewareArgs.next = getNextProxy(middlewareArgs.next, span, prevSpan);
}

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.

Bug: Middleware spans are not closed if next() is not called or an error occurs, leading to a memory leak.
Severity: CRITICAL

🔍 Detailed Analysis

The middleware instrumentation uses startSpanManual to create a span, but this span is only ended when the middleware's next() function is invoked. If a middleware short-circuits the request (a valid pattern for authentication or validation) or throws an error before calling next(), the span is never closed. This leads to an accumulation of unclosed spans in memory, causing a memory leak and performance degradation over time, as well as incomplete tracing data.

💡 Suggested Fix

Wrap the middleware execution logic within a try...finally block. The call to span.end() should be placed in the finally block to ensure that the span is always closed, regardless of whether the middleware completes successfully, throws an error, or short-circuits by not calling next().

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/tanstackstart-react/src/server/middleware.ts#L47-L56

Potential issue: The middleware instrumentation uses `startSpanManual` to create a span,
but this span is only ended when the middleware's `next()` function is invoked. If a
middleware short-circuits the request (a valid pattern for authentication or validation)
or throws an error before calling `next()`, the span is never closed. This leads to an
accumulation of unclosed spans in memory, causing a memory leak and performance
degradation over time, as well as incomplete tracing data.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 8404895

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