diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json
index 5df2d237445e..0076ccf22dc8 100644
--- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json
@@ -13,8 +13,8 @@
},
"dependencies": {
"@sentry/tanstackstart-react": "latest || *",
- "@tanstack/react-start": "^1.139.12",
- "@tanstack/react-router": "^1.139.12",
+ "@tanstack/react-start": "^1.136.0",
+ "@tanstack/react-router": "^1.136.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/test-serverFn.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/test-serverFn.tsx
new file mode 100644
index 000000000000..894ff4e85ee3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/test-serverFn.tsx
@@ -0,0 +1,45 @@
+import { createFileRoute } from '@tanstack/react-router';
+import { createServerFn } from '@tanstack/react-start';
+import { startSpan } from '@sentry/tanstackstart-react';
+
+const testLog = createServerFn().handler(async () => {
+ console.log('Test log from server function');
+ return { message: 'Log created' };
+});
+
+const testNestedLog = createServerFn().handler(async () => {
+ await startSpan({ name: 'testNestedLog' }, async () => {
+ await testLog();
+ });
+
+ console.log('Outer test log from server function');
+ return { message: 'Nested log created' };
+});
+
+export const Route = createFileRoute('/test-serverFn')({
+ component: TestLog,
+});
+
+function TestLog() {
+ return (
+
+
Test Log Page
+
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/server.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/server.ts
new file mode 100644
index 000000000000..b10a3bc1e37b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/server.ts
@@ -0,0 +1,12 @@
+import { wrapFetchWithSentry } from '@sentry/tanstackstart-react';
+
+import handler, { createServerEntry } from '@tanstack/react-start/server-entry';
+import type { ServerEntry } from '@tanstack/react-start/server-entry';
+
+const requestHandler: ServerEntry = wrapFetchWithSentry({
+ fetch(request: Request) {
+ return handler.fetch(request);
+ },
+});
+
+export default createServerEntry(requestHandler);
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts
new file mode 100644
index 000000000000..d2ebbffb0ec0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts
@@ -0,0 +1,100 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Sends a server function transaction with auto-instrumentation', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ !!transactionEvent?.transaction?.startsWith('GET /_serverFn')
+ );
+ });
+
+ await page.goto('/test-serverFn');
+
+ await expect(page.getByText('Call server function', { exact: true })).toBeVisible();
+
+ await page.getByText('Call server function', { exact: true }).click();
+
+ const transactionEvent = await transactionEventPromise;
+
+ // Check for the auto-instrumented server function span
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: expect.stringContaining('GET /_serverFn/'),
+ op: 'function.tanstackstart',
+ origin: 'auto.function.tanstackstart.server',
+ data: {
+ 'sentry.op': 'function.tanstackstart',
+ 'sentry.origin': 'auto.function.tanstackstart.server',
+ 'tanstackstart.function.hash.sha256': expect.any(String),
+ },
+ status: 'ok',
+ }),
+ ]),
+ );
+});
+
+test('Sends a server function transaction for a nested server function only if it is manually instrumented', async ({
+ page,
+}) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ !!transactionEvent?.transaction?.startsWith('GET /_serverFn')
+ );
+ });
+
+ await page.goto('/test-serverFn');
+
+ await expect(page.getByText('Call server function nested')).toBeVisible();
+
+ await page.getByText('Call server function nested').click();
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+
+ // Check for the auto-instrumented server function span
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: expect.stringContaining('GET /_serverFn/'),
+ op: 'function.tanstackstart',
+ origin: 'auto.function.tanstackstart.server',
+ data: {
+ 'sentry.op': 'function.tanstackstart',
+ 'sentry.origin': 'auto.function.tanstackstart.server',
+ 'tanstackstart.function.hash.sha256': expect.any(String),
+ },
+ status: 'ok',
+ }),
+ ]),
+ );
+
+ // Check for the manually instrumented nested span
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'testNestedLog',
+ origin: 'manual',
+ status: 'ok',
+ }),
+ ]),
+ );
+
+ // 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',
+ );
+ const nestedSpan = transactionEvent?.spans?.find(
+ (span: { description?: string; origin?: string }) =>
+ span.description === 'testNestedLog' && span.origin === 'manual',
+ );
+
+ expect(autoSpan).toBeDefined();
+ expect(nestedSpan).toBeDefined();
+ expect(nestedSpan?.parent_span_id).toBe(autoSpan?.span_id);
+});
diff --git a/packages/tanstackstart-react/src/server/index.ts b/packages/tanstackstart-react/src/server/index.ts
index b91a89ed9482..299f1cd85ea9 100644
--- a/packages/tanstackstart-react/src/server/index.ts
+++ b/packages/tanstackstart-react/src/server/index.ts
@@ -4,6 +4,7 @@
export * from '@sentry/node';
export { init } from './sdk';
+export { wrapFetchWithSentry } from './wrapFetchWithSentry';
/**
* A passthrough error boundary for the server that doesn't depend on any react. Error boundaries don't catch SSR errors
diff --git a/packages/tanstackstart-react/src/server/utils.ts b/packages/tanstackstart-react/src/server/utils.ts
new file mode 100644
index 000000000000..a3ebbd118910
--- /dev/null
+++ b/packages/tanstackstart-react/src/server/utils.ts
@@ -0,0 +1,12 @@
+/**
+ * Extracts the SHA-256 hash from a server function pathname.
+ * Server function pathnames are structured as `/_serverFn/`.
+ * This function matches the pattern and returns the hash if found.
+ *
+ * @param pathname - the pathname of the server function
+ * @returns the sha256 of the server function
+ */
+export function extractServerFunctionSha256(pathname: string): string {
+ const serverFnMatch = pathname.match(/\/_serverFn\/([a-f0-9]{64})/i);
+ return serverFnMatch?.[1] ?? 'unknown';
+}
diff --git a/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts b/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts
new file mode 100644
index 000000000000..22d218ef0b48
--- /dev/null
+++ b/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts
@@ -0,0 +1,68 @@
+import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/node';
+import { extractServerFunctionSha256 } from './utils';
+
+export type ServerEntry = {
+ fetch: (request: Request, opts?: unknown) => Promise | Response;
+};
+
+/**
+ * This function can be used to wrap the server entry request handler to add tracing to server-side functionality.
+ * You must explicitly define a server entry point in your application for this to work. This is done by passing the request handler to the `createServerEntry` function.
+ * For more information about the server entry point, see the [TanStack Start documentation](https://tanstack.com/start/docs/server-entry).
+ *
+ * @example
+ * ```ts
+ * import { wrapFetchWithSentry } from '@sentry/tanstackstart-react';
+ *
+ * import handler, { createServerEntry } from '@tanstack/react-start/server-entry';
+ * import type { ServerEntry } from '@tanstack/react-start/server-entry';
+ *
+ * const requestHandler: ServerEntry = wrapFetchWithSentry({
+ * fetch(request: Request) {
+ * return handler.fetch(request);
+ * },
+ * });
+ *
+ * export default serverEntry = createServerEntry(requestHandler);
+ * ```
+ *
+ * @param serverEntry - request handler to wrap
+ * @returns - wrapped request handler
+ */
+export function wrapFetchWithSentry(serverEntry: ServerEntry): ServerEntry {
+ if (serverEntry.fetch) {
+ serverEntry.fetch = new Proxy(serverEntry.fetch, {
+ apply: (target, thisArg, args) => {
+ const request: Request = args[0];
+ const url = new URL(request.url);
+ const method = request.method || 'GET';
+
+ // instrument server functions
+ if (url.pathname.includes('_serverFn') || url.pathname.includes('createServerFn')) {
+ const functionSha256 = extractServerFunctionSha256(url.pathname);
+ const op = 'function.tanstackstart';
+
+ const serverFunctionSpanAttributes = {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.tanstackstart.server',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op,
+ 'tanstackstart.function.hash.sha256': functionSha256,
+ };
+
+ return startSpan(
+ {
+ op: op,
+ name: `${method} ${url.pathname}`,
+ attributes: serverFunctionSpanAttributes,
+ },
+ () => {
+ return target.apply(thisArg, args);
+ },
+ );
+ }
+
+ return target.apply(thisArg, args);
+ },
+ });
+ }
+ return serverEntry;
+}
diff --git a/packages/tanstackstart-react/test/server/utils.test.ts b/packages/tanstackstart-react/test/server/utils.test.ts
new file mode 100644
index 000000000000..f9dd652bd9b1
--- /dev/null
+++ b/packages/tanstackstart-react/test/server/utils.test.ts
@@ -0,0 +1,41 @@
+import { describe, expect, it } from 'vitest';
+import { extractServerFunctionSha256 } from '../../src/server/utils';
+
+describe('extractServerFunctionSha256', () => {
+ it('extracts SHA256 hash from valid server function pathname', () => {
+ const pathname = '/_serverFn/1ac31c23f613ec7e58631cf789642e2feb86c58e3128324cf00d746474a044bf';
+ const result = extractServerFunctionSha256(pathname);
+ expect(result).toBe('1ac31c23f613ec7e58631cf789642e2feb86c58e3128324cf00d746474a044bf');
+ });
+
+ it('extracts SHA256 hash from valid server function pathname that is a subpath', () => {
+ const pathname = '/api/_serverFn/1ac31c23f613ec7e58631cf789642e2feb86c58e3128324cf00d746474a044bf';
+ const result = extractServerFunctionSha256(pathname);
+ expect(result).toBe('1ac31c23f613ec7e58631cf789642e2feb86c58e3128324cf00d746474a044bf');
+ });
+
+ it('extracts SHA256 hash from valid server function pathname with query parameters', () => {
+ const pathname = '/_serverFn/1ac31c23f613ec7e58631cf789642e2feb86c58e3128324cf00d746474a044bf?param=value';
+ const result = extractServerFunctionSha256(pathname);
+ expect(result).toBe('1ac31c23f613ec7e58631cf789642e2feb86c58e3128324cf00d746474a044bf');
+ });
+
+ it('extracts SHA256 hash with uppercase hex characters', () => {
+ const pathname = '/_serverFn/1AC31C23F613EC7E58631CF789642E2FEB86C58E3128324CF00D746474A044BF';
+ const result = extractServerFunctionSha256(pathname);
+ expect(result).toBe('1AC31C23F613EC7E58631CF789642E2FEB86C58E3128324CF00D746474A044BF');
+ });
+
+ it('returns unknown for pathname without server function pattern', () => {
+ const pathname = '/api/users/123';
+ const result = extractServerFunctionSha256(pathname);
+ expect(result).toBe('unknown');
+ });
+
+ it('returns unknown for pathname with incomplete hash', () => {
+ // Hash is too short (only 32 chars instead of 64)
+ const pathname = '/_serverFn/1ac31c23f613ec7e58631cf789642e2f';
+ const result = extractServerFunctionSha256(pathname);
+ expect(result).toBe('unknown');
+ });
+});