Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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();
});

// Manually wrap middlewares with Sentry
export const [
wrappedGlobalRequestMiddleware,
wrappedGlobalFunctionMiddleware,
wrappedServerFnMiddleware,
wrappedServerRouteRequestMiddleware,
] = wrapMiddlewaresWithSentry({
globalRequestMiddleware,
globalFunctionMiddleware,
serverFnMiddleware,
serverRouteRequestMiddleware,
});
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,47 @@
import { createFileRoute } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
import { wrappedServerFnMiddleware } 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' };
});

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>
</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,135 @@
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',
}),
]),
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,19 @@ test('Sends a server function transaction for a nested server function only if i
]),
);

// Verify that the auto span is the parent of the nested span
const autoSpan = transactionEvent?.spans?.find(
(span: { op?: string; origin?: string }) =>
span.op === 'function.tanstackstart' && span.origin === 'auto.function.tanstackstart.server',
// Verify that globalFunctionMiddleware and testNestedLog are sibling spans under the root
const functionMiddlewareSpan = transactionEvent?.spans?.find(
(span: { description?: string; origin?: string }) =>
span.description === 'globalFunctionMiddleware' && span.origin === 'manual.middleware.tanstackstart',
);
const nestedSpan = transactionEvent?.spans?.find(
(span: { description?: string; origin?: string }) =>
span.description === 'testNestedLog' && span.origin === 'manual',
);

expect(autoSpan).toBeDefined();
expect(functionMiddlewareSpan).toBeDefined();
expect(nestedSpan).toBeDefined();
expect(nestedSpan?.parent_span_id).toBe(autoSpan?.span_id);

// Both spans should be siblings under the same parent (root transaction)
expect(nestedSpan?.parent_span_id).toBe(functionMiddlewareSpan?.parent_span_id);
});
10 changes: 10 additions & 0 deletions packages/tanstackstart-react/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
// import/export got a false positive, and affects most of our index barrel files
// can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703
/* eslint-disable import/export */
import type { TanStackMiddlewareBase } from '../common/types';

export * from '@sentry/react';

export { init } from './sdk';

/**
* No-op stub for client-side builds.
* The actual implementation is server-only, but this stub is needed to prevent build errors.
*/
export function wrapMiddlewaresWithSentry<T extends TanStackMiddlewareBase>(middlewares: Record<string, T>): T[] {
return Object.values(middlewares);
}
2 changes: 1 addition & 1 deletion packages/tanstackstart-react/src/common/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export {};
export type { TanStackMiddlewareBase, MiddlewareWrapperOptions } from './types';
7 changes: 7 additions & 0 deletions packages/tanstackstart-react/src/common/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type TanStackMiddlewareBase = {
options?: { server?: (...args: unknown[]) => unknown };
};

export type MiddlewareWrapperOptions = {
name: string;
};
2 changes: 1 addition & 1 deletion packages/tanstackstart-react/src/index.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
// so we keep this to be future proof
export * from './client';
// nothing gets exported yet from there
// eslint-disable-next-line import/export

export * from './common';
2 changes: 2 additions & 0 deletions packages/tanstackstart-react/src/index.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegra
export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook;
export declare const statsigIntegration: typeof clientSdk.statsigIntegration;
export declare const unleashIntegration: typeof clientSdk.unleashIntegration;

export declare const wrapMiddlewaresWithSentry: typeof serverSdk.wrapMiddlewaresWithSentry;
1 change: 1 addition & 0 deletions packages/tanstackstart-react/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from '@sentry/node';

export { init } from './sdk';
export { wrapFetchWithSentry } from './wrapFetchWithSentry';
export { wrapMiddlewaresWithSentry } from './middleware';

/**
* A passthrough error boundary for the server that doesn't depend on any react. Error boundaries don't catch SSR errors
Expand Down
Loading
Loading