Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@

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

- **feat(tanstackstart-react): Add `wrapMiddlewaresWithSentry` for manual middleware instrumentation**

You can now wrap your middlewares using `wrapMiddlewaresWithSentry`, allowing you to trace middleware execution in your TanStack Start application.

```ts
import { createMiddleware } from '@tanstack/react-start';
import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';

const loggingMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => {
console.log('Request started');
return next();
});

export const [wrappedLoggingMiddleware] = wrapMiddlewaresWithSentry({ loggingMiddleware });
```

## 10.33.0

### Important Changes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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 }) => {
console.log('Global request middleware executed');
return next();
});

// Global function middleware - runs on every server function
const globalFunctionMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => {
console.log('Global function middleware executed');
return next();
});

// Server function middleware
const serverFnMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => {
console.log('Server function middleware executed');
return next();
});

// Server route request middleware
const serverRouteRequestMiddleware = createMiddleware().server(async ({ next }) => {
console.log('Server route request middleware executed');
return next();
});

// Early return middleware - returns without calling next()
const earlyReturnMiddleware = createMiddleware({ type: 'function' }).server(async () => {
console.log('Early return middleware executed - not calling next()');
return { earlyReturn: true, message: 'Middleware returned early without calling next()' };
});

// Error middleware - throws an exception
const errorMiddleware = createMiddleware({ type: 'function' }).server(async () => {
console.log('Error middleware executed - throwing error');
throw new Error('Middleware Error Test');
});

// Manually wrap middlewares with Sentry
export const [
wrappedGlobalRequestMiddleware,
wrappedGlobalFunctionMiddleware,
wrappedServerFnMiddleware,
wrappedServerRouteRequestMiddleware,
wrappedEarlyReturnMiddleware,
wrappedErrorMiddleware,
] = wrapMiddlewaresWithSentry({
globalRequestMiddleware,
globalFunctionMiddleware,
serverFnMiddleware,
serverRouteRequestMiddleware,
earlyReturnMiddleware,
errorMiddleware,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createFileRoute } from '@tanstack/react-router';
import { wrappedServerRouteRequestMiddleware } from '../middleware';

export const Route = createFileRoute('/api/test-middleware')({
server: {
middleware: [wrappedServerRouteRequestMiddleware],
handlers: {
GET: async () => {
return { message: 'Server route middleware test' };
},
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { createFileRoute } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
import { wrappedServerFnMiddleware, wrappedEarlyReturnMiddleware, wrappedErrorMiddleware } from '../middleware';

// Server function with specific middleware (also gets global function middleware)
const serverFnWithMiddleware = createServerFn()
.middleware([wrappedServerFnMiddleware])
.handler(async () => {
console.log('Server function with specific middleware executed');
return { message: 'Server function middleware test' };
});

// Server function without specific middleware (only gets global function middleware)
const serverFnWithoutMiddleware = createServerFn().handler(async () => {
console.log('Server function without specific middleware executed');
return { message: 'Global middleware only test' };
});

// Server function with early return middleware (middleware returns without calling next)
const serverFnWithEarlyReturnMiddleware = createServerFn()
.middleware([wrappedEarlyReturnMiddleware])
.handler(async () => {
console.log('This should not be executed - middleware returned early');
return { message: 'This should not be returned' };
});

// Server function with error middleware (middleware throws an error)
const serverFnWithErrorMiddleware = createServerFn()
.middleware([wrappedErrorMiddleware])
.handler(async () => {
console.log('This should not be executed - middleware threw error');
return { message: 'This should not be returned' };
});

export const Route = createFileRoute('/test-middleware')({
component: TestMiddleware,
});

function TestMiddleware() {
return (
<div>
<h1>Test Middleware Page</h1>
<button
id="server-fn-middleware-btn"
type="button"
onClick={async () => {
await serverFnWithMiddleware();
}}
>
Call server function with middleware
</button>
<button
id="server-fn-global-only-btn"
type="button"
onClick={async () => {
await serverFnWithoutMiddleware();
}}
>
Call server function (global middleware only)
</button>
<button
id="server-fn-early-return-btn"
type="button"
onClick={async () => {
const result = await serverFnWithEarlyReturnMiddleware();
console.log('Early return result:', result);
}}
>
Call server function with early return middleware
</button>
<button
id="server-fn-error-btn"
type="button"
onClick={async () => {
try {
await serverFnWithErrorMiddleware();
} catch (error) {
console.log('Caught error from middleware:', error);
}
}}
>
Call server function with error middleware
</button>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createStart } from '@tanstack/react-start';
import { wrappedGlobalRequestMiddleware, wrappedGlobalFunctionMiddleware } from './middleware';

export const startInstance = createStart(() => {
return {
requestMiddleware: [wrappedGlobalRequestMiddleware],
functionMiddleware: [wrappedGlobalFunctionMiddleware],
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

test('Sends spans for multiple middlewares and verifies they are siblings under the same parent span', async ({
page,
}) => {
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
!!transactionEvent?.transaction?.startsWith('GET /_serverFn')
);
});

await page.goto('/test-middleware');
await expect(page.locator('#server-fn-middleware-btn')).toBeVisible();
await page.locator('#server-fn-middleware-btn').click();

const transactionEvent = await transactionEventPromise;

expect(Array.isArray(transactionEvent?.spans)).toBe(true);

// Find both middleware spans
const serverFnMiddlewareSpan = transactionEvent?.spans?.find(
(span: { description?: string; origin?: string }) =>
span.description === 'serverFnMiddleware' && span.origin === 'manual.middleware.tanstackstart',
);
const globalFunctionMiddlewareSpan = transactionEvent?.spans?.find(
(span: { description?: string; origin?: string }) =>
span.description === 'globalFunctionMiddleware' && span.origin === 'manual.middleware.tanstackstart',
);

// Verify both middleware spans exist with expected properties
expect(serverFnMiddlewareSpan).toEqual(
expect.objectContaining({
description: 'serverFnMiddleware',
op: 'middleware.tanstackstart',
origin: 'manual.middleware.tanstackstart',
status: 'ok',
}),
);
expect(globalFunctionMiddlewareSpan).toEqual(
expect.objectContaining({
description: 'globalFunctionMiddleware',
op: 'middleware.tanstackstart',
origin: 'manual.middleware.tanstackstart',
status: 'ok',
}),
);

// Both middleware spans should be siblings under the same parent
expect(serverFnMiddlewareSpan?.parent_span_id).toBe(globalFunctionMiddlewareSpan?.parent_span_id);
});

test('Sends spans for global function middleware', async ({ page }) => {
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
!!transactionEvent?.transaction?.startsWith('GET /_serverFn')
);
});

await page.goto('/test-middleware');
await expect(page.locator('#server-fn-global-only-btn')).toBeVisible();
await page.locator('#server-fn-global-only-btn').click();

const transactionEvent = await transactionEventPromise;

expect(Array.isArray(transactionEvent?.spans)).toBe(true);

// Check for the global function middleware span
expect(transactionEvent?.spans).toEqual(
expect.arrayContaining([
expect.objectContaining({
description: 'globalFunctionMiddleware',
op: 'middleware.tanstackstart',
origin: 'manual.middleware.tanstackstart',
status: 'ok',
}),
]),
);
});

test('Sends spans for global request middleware', async ({ page }) => {
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
transactionEvent?.transaction === 'GET /test-middleware'
);
});

await page.goto('/test-middleware');

const transactionEvent = await transactionEventPromise;

expect(Array.isArray(transactionEvent?.spans)).toBe(true);

// Check for the global request middleware span
expect(transactionEvent?.spans).toEqual(
expect.arrayContaining([
expect.objectContaining({
description: 'globalRequestMiddleware',
op: 'middleware.tanstackstart',
origin: 'manual.middleware.tanstackstart',
status: 'ok',
}),
]),
);
});

test('Sends spans for server route request middleware', async ({ page }) => {
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
transactionEvent?.transaction === 'GET /api/test-middleware'
);
});

await page.goto('/api/test-middleware');

const transactionEvent = await transactionEventPromise;

expect(Array.isArray(transactionEvent?.spans)).toBe(true);

// Check for the server route request middleware span
expect(transactionEvent?.spans).toEqual(
expect.arrayContaining([
expect.objectContaining({
description: 'serverRouteRequestMiddleware',
op: 'middleware.tanstackstart',
origin: 'manual.middleware.tanstackstart',
status: 'ok',
}),
]),
);
});

test('Sends span for middleware that returns early without calling next()', async ({ page }) => {
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
!!transactionEvent?.transaction?.startsWith('GET /_serverFn')
);
});

await page.goto('/test-middleware');
await expect(page.locator('#server-fn-early-return-btn')).toBeVisible();
await page.locator('#server-fn-early-return-btn').click();

const transactionEvent = await transactionEventPromise;

expect(Array.isArray(transactionEvent?.spans)).toBe(true);

// Check for the early return middleware span
expect(transactionEvent?.spans).toEqual(
expect.arrayContaining([
expect.objectContaining({
description: 'earlyReturnMiddleware',
op: 'middleware.tanstackstart',
origin: 'manual.middleware.tanstackstart',
status: 'ok',
}),
]),
);
});

test('Sends span for middleware that throws an error', async ({ page }) => {
const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
!!transactionEvent?.transaction?.startsWith('GET /_serverFn')
);
});

await page.goto('/test-middleware');
await expect(page.locator('#server-fn-error-btn')).toBeVisible();
await page.locator('#server-fn-error-btn').click();

const transactionEvent = await transactionEventPromise;

expect(Array.isArray(transactionEvent?.spans)).toBe(true);

// Check for the error middleware span
expect(transactionEvent?.spans).toEqual(
expect.arrayContaining([
expect.objectContaining({
description: 'errorMiddleware',
op: 'middleware.tanstackstart',
origin: 'manual.middleware.tanstackstart',
}),
]),
);
});
Loading
Loading