-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(tanstackstart-react): Trace server functions #18500
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
bdad818
Fix e2e version to minimally supported
nicohrubec a3f5e05
Add default server entry with some logs to e2e test
nicohrubec 320aa98
add basic failing transaction test
nicohrubec c1d5251
proxy fetch with withSentry
nicohrubec 8dd117a
lint
nicohrubec 6de8816
make it work
nicohrubec 0f425fd
stuff works
nicohrubec 076dc79
remove redundant awaits
nicohrubec d7f246d
fix ServerEntry type and remove async
nicohrubec 2cb31a8
clean out logs
nicohrubec c5246a6
Add test for nested server functions
nicohrubec fb4736e
Origin is now server instead of serverFn
nicohrubec f14b8a3
Remove async
nicohrubec 3a1c4a2
Improve span name: add request method name and use path name instead …
nicohrubec 6eedc35
Add server function sha256 as span attribute
nicohrubec 4d9b2cb
yarn fix
nicohrubec 09ba63c
make sha256 extraction more defensive to never be undefined + add uni…
nicohrubec 9dc3d24
yarn fix
nicohrubec 59a790d
withSentry becomes wrapFetchWithSentry
nicohrubec a8f317e
Merge branch 'develop' into nh/tss-server-fn-tracing
nicohrubec File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/test-serverFn.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div> | ||
| <h1>Test Log Page</h1> | ||
| <button | ||
| type="button" | ||
| onClick={async () => { | ||
| await testLog(); | ||
| }} | ||
| > | ||
| Call server function | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={async () => { | ||
| await testNestedLog(); | ||
| }} | ||
| > | ||
| Call server function nested | ||
| </button> | ||
| </div> | ||
| ); | ||
| } |
12 changes: 12 additions & 0 deletions
12
dev-packages/e2e-tests/test-applications/tanstackstart-react/src/server.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); |
100 changes: 100 additions & 0 deletions
100
dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| /** | ||
| * Extracts the SHA-256 hash from a server function pathname. | ||
| * Server function pathnames are structured as `/_serverFn/<hash>`. | ||
| * 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'; | ||
| } |
68 changes: 68 additions & 0 deletions
68
packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | 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<typeof serverEntry.fetch>(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, | ||
andreiborza marked this conversation as resolved.
Show resolved
Hide resolved
nicohrubec marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| return startSpan( | ||
| { | ||
| op: op, | ||
| name: `${method} ${url.pathname}`, | ||
| attributes: serverFunctionSpanAttributes, | ||
| }, | ||
| () => { | ||
| return target.apply(thisArg, args); | ||
| }, | ||
| ); | ||
nicohrubec marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| return target.apply(thisArg, args); | ||
| }, | ||
| }); | ||
| } | ||
| return serverEntry; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.