diff --git a/CHANGELOG.md b/CHANGELOG.md index 356bc6fa31c4..f567a188da48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ Server transaction names are now parametrized automatically (e.g., `GET /users/123` becomes `GET /users/$userId`), improving transaction grouping in Sentry. +- **feat(tanstackstart-react): Show readable server function names in traces ([#21190](https://github.com/getsentry/sentry-javascript/pull/21190))** + + Server function spans now show human-readable names (e.g., `GET /_serverFn/greet` instead of `GET /_serverFn/a10e70b3...`). The `tanstackstart.function.hash.sha256` span attribute has been renamed to `tanstackstart.function.id`. + ## 10.54.0 ### Important Changes diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/transaction.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/transaction.test.ts index a47cfdfa298e..e884ac474b3a 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/transaction.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/transaction.test.ts @@ -25,14 +25,16 @@ test('Sends a server function transaction with span from wrapFetchWithSentry', a expect(transactionEvent?.spans).toHaveLength(1); expect(transactionEvent?.spans).toEqual([ expect.objectContaining({ - description: expect.stringContaining('GET /_serverFn/'), + description: 'GET /_serverFn/testLog', op: 'function.tanstackstart', origin: 'auto.function.tanstackstart.server', - data: expect.objectContaining({ + data: { 'sentry.op': 'function.tanstackstart', 'sentry.origin': 'auto.function.tanstackstart.server', - 'tanstackstart.function.hash.sha256': expect.any(String), - }), + 'sentry.source': 'route', + 'tanstackstart.function.id': expect.any(String), + 'tanstackstart.function.filename': 'src/routes/test-serverFn.tsx', + }, }), ]); }); @@ -62,14 +64,16 @@ test('Sends a server function transaction for a nested server function with manu expect(transactionEvent?.spans).toEqual( expect.arrayContaining([ expect.objectContaining({ - description: expect.stringContaining('GET /_serverFn/'), + description: 'GET /_serverFn/testNestedLog', op: 'function.tanstackstart', origin: 'auto.function.tanstackstart.server', - data: expect.objectContaining({ + data: { 'sentry.op': 'function.tanstackstart', 'sentry.origin': 'auto.function.tanstackstart.server', - 'tanstackstart.function.hash.sha256': expect.any(String), - }), + 'sentry.source': 'route', + 'tanstackstart.function.id': expect.any(String), + 'tanstackstart.function.filename': 'src/routes/test-serverFn.tsx', + }, }), expect.objectContaining({ description: 'testNestedLog', 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 index f5e70a676432..621ee3f88e02 100644 --- 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 @@ -27,13 +27,15 @@ test('Sends a server function transaction with auto-instrumentation', async ({ p expect(transactionEvent?.spans).toEqual( expect.arrayContaining([ expect.objectContaining({ - description: expect.stringContaining('GET /_serverFn/'), + description: 'GET /_serverFn/testLog', 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), + 'sentry.source': 'route', + 'tanstackstart.function.id': expect.any(String), + 'tanstackstart.function.filename': 'src/routes/test-serverFn.tsx', }, status: 'ok', }), @@ -65,13 +67,15 @@ test('Sends a server function transaction for a nested server function only if i expect(transactionEvent?.spans).toEqual( expect.arrayContaining([ expect.objectContaining({ - description: expect.stringContaining('GET /_serverFn/'), + description: 'GET /_serverFn/testNestedLog', 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), + 'sentry.source': 'route', + 'tanstackstart.function.id': expect.any(String), + 'tanstackstart.function.filename': 'src/routes/test-serverFn.tsx', }, status: 'ok', }), diff --git a/packages/tanstackstart-react/src/server/globalMiddleware.ts b/packages/tanstackstart-react/src/server/globalMiddleware.ts index 516535da41c5..882e06c59f8e 100644 --- a/packages/tanstackstart-react/src/server/globalMiddleware.ts +++ b/packages/tanstackstart-react/src/server/globalMiddleware.ts @@ -1,7 +1,20 @@ -import { addNonEnumerableProperty, captureException } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + addNonEnumerableProperty, + captureException, + getActiveSpan, + spanToJSON, + updateSpanName, +} from '@sentry/core'; import type { TanStackMiddlewareBase } from '../common/types'; import { SENTRY_INTERNAL } from './middleware'; +type ServerFnMeta = { + id?: string; + name?: string; + filename?: string; +}; + function createSentryMiddlewareHandler(mechanismType: string) { return async function sentryMiddlewareHandler({ next }: { next: () => Promise }): Promise { try { @@ -15,6 +28,41 @@ function createSentryMiddlewareHandler(mechanismType: string) { }; } +function createSentryFunctionMiddlewareHandler(mechanismType: string) { + return async function sentryFunctionMiddlewareHandler({ + next, + serverFnMeta, + }: { + next: () => Promise; + serverFnMeta?: ServerFnMeta; + }): Promise { + const activeSpan = getActiveSpan(); + const spanData = activeSpan ? spanToJSON(activeSpan) : undefined; + if (activeSpan && spanData?.op === 'function.tanstackstart') { + if (serverFnMeta?.name) { + const method = spanData.description?.split(' ')[0] || 'GET'; + updateSpanName(activeSpan, `${method} /_serverFn/${serverFnMeta.name}`); + activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + } + if (serverFnMeta?.id) { + activeSpan.setAttribute('tanstackstart.function.id', serverFnMeta.id); + } + if (serverFnMeta?.filename) { + activeSpan.setAttribute('tanstackstart.function.filename', serverFnMeta.filename); + } + } + + try { + return await next(); + } catch (e) { + captureException(e, { + mechanism: { type: mechanismType, handled: false }, + }); + throw e; + } + }; +} + /** * Global request middleware that captures errors from API route requests. * Should be added as the first entry in the `requestMiddleware` array of `createStart()`. @@ -36,8 +84,9 @@ export const sentryGlobalFunctionMiddleware: TanStackMiddlewareBase = { '~types': undefined, options: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - server: createSentryMiddlewareHandler('auto.middleware.tanstackstart.server_function') as (...args: any[]) => any, + server: createSentryFunctionMiddlewareHandler('auto.middleware.tanstackstart.server_function') as ( + ...args: any[] // eslint-disable-line @typescript-eslint/no-explicit-any + ) => any, // eslint-disable-line @typescript-eslint/no-explicit-any }, }; diff --git a/packages/tanstackstart-react/src/server/utils.ts b/packages/tanstackstart-react/src/server/utils.ts index 2a94cb79deac..9ff87c8ebbf4 100644 --- a/packages/tanstackstart-react/src/server/utils.ts +++ b/packages/tanstackstart-react/src/server/utils.ts @@ -1,19 +1,6 @@ import type { StartSpanOptions } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/node'; -/** - * 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'; -} - /** * Returns span options for TanStack Start middleware spans. */ diff --git a/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts b/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts index 73ea5604959e..fab2788cd234 100644 --- a/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts +++ b/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts @@ -6,7 +6,6 @@ import { startSpan, } from '@sentry/node'; import { updateSpanWithRouteParametrization } from './routeParametrization'; -import { extractServerFunctionSha256 } from './utils'; declare const __SENTRY_ROUTE_PATTERNS__: string[] | undefined; @@ -143,20 +142,16 @@ export function wrapFetchWithSentry(serverEntry: ServerEntry): ServerEntry { // 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 await startSpan( { - op: op, + op, name: `${method} ${url.pathname}`, - attributes: serverFunctionSpanAttributes, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.tanstackstart.server', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, + }, }, async () => { return target.apply(thisArg, args); diff --git a/packages/tanstackstart-react/test/server/utils.test.ts b/packages/tanstackstart-react/test/server/utils.test.ts index f9dd652bd9b1..66540bf3dfb3 100644 --- a/packages/tanstackstart-react/test/server/utils.test.ts +++ b/packages/tanstackstart-react/test/server/utils.test.ts @@ -1,41 +1,16 @@ import { describe, expect, it } from 'vitest'; -import { extractServerFunctionSha256 } from '../../src/server/utils'; +import { getMiddlewareSpanOptions } 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'); +describe('getMiddlewareSpanOptions', () => { + it('returns correct span options', () => { + const options = getMiddlewareSpanOptions('testMiddleware'); + expect(options).toEqual({ + op: 'middleware.tanstackstart', + name: 'testMiddleware', + attributes: { + 'sentry.op': 'middleware.tanstackstart', + 'sentry.origin': 'auto.middleware.tanstackstart', + }, + }); }); }); diff --git a/packages/tanstackstart-react/test/server/wrapFetchWithSentry.test.ts b/packages/tanstackstart-react/test/server/wrapFetchWithSentry.test.ts index 90300a5d52ba..7b2b298f4685 100644 --- a/packages/tanstackstart-react/test/server/wrapFetchWithSentry.test.ts +++ b/packages/tanstackstart-react/test/server/wrapFetchWithSentry.test.ts @@ -50,7 +50,7 @@ describe('wrapFetchWithSentry', () => { expect(flushIfServerlessSpy).toHaveBeenCalledTimes(1); }); - it('calls flushIfServerless after a server function request', async () => { + it('creates a function.tanstackstart span for server function requests', async () => { const mockResponse = new Response('ok'); const fetchFn = vi.fn().mockResolvedValue(mockResponse); @@ -59,7 +59,13 @@ describe('wrapFetchWithSentry', () => { await serverEntry.fetch(request); - expect(startSpanSpy).toHaveBeenCalled(); + expect(startSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + op: 'function.tanstackstart', + name: 'GET /_serverFn/abc123', + }), + expect.any(Function), + ); expect(flushIfServerlessSpy).toHaveBeenCalledTimes(1); });