diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.hello.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.hello.ts new file mode 100644 index 000000000000..6508439a9b33 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.hello.ts @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/api/hello')({ + server: { + handlers: { + GET: async () => { + return new Response('Hello, world!'); + }, + }, + }, +}); 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 d2ebbffb0ec0..78e860e309b3 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 @@ -98,3 +98,37 @@ test('Sends a server function transaction for a nested server function only if i expect(nestedSpan).toBeDefined(); expect(nestedSpan?.parent_span_id).toBe(autoSpan?.span_id); }); + +test('Sends an API route transaction with auto-instrumentation', async ({ page }) => { + const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /api/hello' + ); + }); + + await page.goto('/api/hello'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: 'GET /api/hello', + }), + ); + + expect(transactionEvent?.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'GET /api/hello', + op: 'http.server', + origin: 'auto.http.tanstackstart.server', + data: { + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.tanstackstart.server', + 'sentry.source': 'url', + 'http.request.method': 'GET', + }, + }), + ]), + ); +}); diff --git a/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts b/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts index 22d218ef0b48..788873db9970 100644 --- a/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts +++ b/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts @@ -1,4 +1,11 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/node'; +import type { SpanAttributes } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + startSpan, +} from '@sentry/node'; import { extractServerFunctionSha256 } from './utils'; export type ServerEntry = { @@ -37,30 +44,39 @@ export function wrapFetchWithSentry(serverEntry: ServerEntry): ServerEntry { const url = new URL(request.url); const method = request.method || 'GET'; - // instrument server functions + let op: string; + let spanAttributes: SpanAttributes; + if (url.pathname.includes('_serverFn') || url.pathname.includes('createServerFn')) { + // server function call + op = 'function.tanstackstart'; const functionSha256 = extractServerFunctionSha256(url.pathname); - const op = 'function.tanstackstart'; - - const serverFunctionSpanAttributes = { + spanAttributes = { [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); - }, - ); + } else { + // API route or other server request + op = 'http.server'; + spanAttributes = { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.tanstackstart.server', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, + [SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD]: method, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }; } - return target.apply(thisArg, args); + return startSpan( + { + op, + name: `${method} ${url.pathname}`, + attributes: spanAttributes, + }, + () => { + return target.apply(thisArg, args); + }, + ); }, }); }