-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(solidstart): Add server action instrumentation helper (#13035)
Can be used like this: ```js const getUserData = async () => { 'use server'; return await withServerActionInstrumentation('getData', () => { return { prefecture: 'Kanagawa' }; }); }; ``` Can also be used for api routes like this: ```js export async function GET() { return await withServerActionInstrumentation('getUser', () => { return json({ prefecture: 'Akita' }) }) } ```
- Loading branch information
1 parent
7dc6a25
commit 71af30f
Showing
16 changed files
with
392 additions
and
7 deletions.
There are no files selected for viewing
This file contains 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 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 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 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
17 changes: 17 additions & 0 deletions
17
dev-packages/e2e-tests/test-applications/solidstart/src/routes/server-error.tsx
This file contains 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,17 @@ | ||
import { withServerActionInstrumentation } from '@sentry/solidstart'; | ||
import { createAsync } from '@solidjs/router'; | ||
|
||
const getPrefecture = async () => { | ||
'use server'; | ||
return await withServerActionInstrumentation('getPrefecture', () => { | ||
throw new Error('Error thrown from Solid Start E2E test app server route'); | ||
|
||
return { prefecture: 'Kanagawa' }; | ||
}); | ||
}; | ||
|
||
export default function ServerErrorPage() { | ||
const data = createAsync(() => getPrefecture()); | ||
|
||
return <div>Prefecture: {data()?.prefecture}</div>; | ||
} |
19 changes: 17 additions & 2 deletions
19
dev-packages/e2e-tests/test-applications/solidstart/src/routes/users/[id].tsx
This file contains 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 |
---|---|---|
@@ -1,6 +1,21 @@ | ||
import { useParams } from '@solidjs/router'; | ||
import { withServerActionInstrumentation } from '@sentry/solidstart'; | ||
import { createAsync, useParams } from '@solidjs/router'; | ||
|
||
const getPrefecture = async () => { | ||
'use server'; | ||
return await withServerActionInstrumentation('getPrefecture', () => { | ||
return { prefecture: 'Ehime' }; | ||
}); | ||
}; | ||
export default function User() { | ||
const params = useParams(); | ||
return <div>User ID: {params.id}</div>; | ||
const userData = createAsync(() => getPrefecture()); | ||
|
||
return ( | ||
<div> | ||
User ID: {params.id} | ||
<br /> | ||
Prefecture: {userData()?.prefecture} | ||
</div> | ||
); | ||
} |
31 changes: 31 additions & 0 deletions
31
dev-packages/e2e-tests/test-applications/solidstart/tests/errors.server.test.ts
This file contains 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,31 @@ | ||
import { expect, test } from '@playwright/test'; | ||
import { waitForError } from '@sentry-internal/test-utils'; | ||
|
||
test.describe('server-side errors', () => { | ||
test('captures server action error', async ({ page }) => { | ||
const errorEventPromise = waitForError('solidstart', errorEvent => { | ||
return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Solid Start E2E test app server route'; | ||
}); | ||
|
||
await page.goto(`/server-error`); | ||
|
||
const error = await errorEventPromise; | ||
|
||
expect(error.tags).toMatchObject({ runtime: 'node' }); | ||
expect(error).toMatchObject({ | ||
exception: { | ||
values: [ | ||
{ | ||
type: 'Error', | ||
value: 'Error thrown from Solid Start E2E test app server route', | ||
mechanism: { | ||
type: 'solidstart', | ||
handled: false, | ||
}, | ||
}, | ||
], | ||
}, | ||
transaction: 'GET /server-error', | ||
}); | ||
}); | ||
}); |
55 changes: 55 additions & 0 deletions
55
dev-packages/e2e-tests/test-applications/solidstart/tests/performance.server.test.ts
This file contains 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,55 @@ | ||
import { expect, test } from '@playwright/test'; | ||
import { waitForTransaction } from '@sentry-internal/test-utils'; | ||
import { | ||
SEMANTIC_ATTRIBUTE_SENTRY_OP, | ||
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, | ||
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, | ||
} from '@sentry/core'; | ||
|
||
test('sends a server action transaction on pageload', async ({ page }) => { | ||
const transactionPromise = waitForTransaction('solidstart', transactionEvent => { | ||
return transactionEvent?.transaction === 'GET /users/6'; | ||
}); | ||
|
||
await page.goto('/users/6'); | ||
|
||
const transaction = await transactionPromise; | ||
|
||
expect(transaction.spans).toEqual( | ||
expect.arrayContaining([ | ||
expect.objectContaining({ | ||
description: 'getPrefecture', | ||
data: { | ||
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action', | ||
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', | ||
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', | ||
}, | ||
}), | ||
]), | ||
); | ||
}); | ||
|
||
test('sends a server action transaction on client navigation', async ({ page }) => { | ||
const transactionPromise = waitForTransaction('solidstart', transactionEvent => { | ||
return transactionEvent?.transaction === 'POST getPrefecture'; | ||
}); | ||
|
||
await page.goto('/'); | ||
await page.locator('#navLink').click(); | ||
await page.waitForURL('/users/5'); | ||
|
||
const transaction = await transactionPromise; | ||
|
||
expect(transaction.spans).toEqual( | ||
expect.arrayContaining([ | ||
expect.objectContaining({ | ||
description: 'getPrefecture', | ||
data: { | ||
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action', | ||
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', | ||
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', | ||
}, | ||
}), | ||
]), | ||
); | ||
}); |
This file contains 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 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 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 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 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,33 @@ | ||
import { flush } from '@sentry/node'; | ||
import { logger } from '@sentry/utils'; | ||
import { DEBUG_BUILD } from '../common/debug-build'; | ||
|
||
/** Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda ends */ | ||
export async function flushIfServerless(): Promise<void> { | ||
const isServerless = !!process.env.LAMBDA_TASK_ROOT || !!process.env.VERCEL; | ||
|
||
if (isServerless) { | ||
try { | ||
DEBUG_BUILD && logger.log('Flushing events...'); | ||
await flush(2000); | ||
DEBUG_BUILD && logger.log('Done flushing events'); | ||
} catch (e) { | ||
DEBUG_BUILD && logger.log('Error while flushing events:\n', e); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Determines if a thrown "error" is a redirect Response which Solid Start users can throw to redirect to another route. | ||
* see: https://docs.solidjs.com/solid-router/reference/data-apis/response-helpers#redirect | ||
* @param error the potential redirect error | ||
*/ | ||
export function isRedirect(error: unknown): boolean { | ||
if (error == null || !(error instanceof Response)) { | ||
return false; | ||
} | ||
|
||
const hasValidLocation = typeof error.headers.get('location') === 'string'; | ||
const hasValidStatus = error.status >= 300 && error.status <= 308; | ||
return hasValidLocation && hasValidStatus; | ||
} |
58 changes: 58 additions & 0 deletions
58
packages/solidstart/src/server/withServerActionInstrumentation.ts
This file contains 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,58 @@ | ||
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, handleCallbackErrors } from '@sentry/core'; | ||
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, getActiveSpan, spanToJSON, startSpan } from '@sentry/node'; | ||
import { flushIfServerless, isRedirect } from './utils'; | ||
|
||
/** | ||
* Wraps a server action (functions that use the 'use server' directive) | ||
* function body with Sentry Error and Performance instrumentation. | ||
*/ | ||
export async function withServerActionInstrumentation<A extends (...args: unknown[]) => unknown>( | ||
serverActionName: string, | ||
callback: A, | ||
): Promise<ReturnType<A>> { | ||
const activeSpan = getActiveSpan(); | ||
|
||
if (activeSpan) { | ||
const spanData = spanToJSON(activeSpan).data; | ||
|
||
// In solid start, server function calls are made to `/_server` which doesn't tell us | ||
// a lot. We rewrite the span's route to be that of the sever action name but only | ||
// if the target is `/_server`, otherwise we'd overwrite pageloads on routes that use | ||
// server actions (which are more meaningful, e.g. a request to `GET /users/5` is more | ||
// meaningful than overwriting it with `GET doSomeFunctionCall`). | ||
if (spanData && !spanData['http.route'] && spanData['http.target'] === '/_server') { | ||
activeSpan.setAttribute('http.route', serverActionName); | ||
activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'component'); | ||
} | ||
} | ||
|
||
try { | ||
return await startSpan( | ||
{ | ||
op: 'function.server_action', | ||
name: serverActionName, | ||
attributes: { | ||
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', | ||
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', | ||
}, | ||
}, | ||
async span => { | ||
const result = await handleCallbackErrors(callback, error => { | ||
if (!isRedirect(error)) { | ||
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); | ||
captureException(error, { | ||
mechanism: { | ||
handled: false, | ||
type: 'solidstart', | ||
}, | ||
}); | ||
} | ||
}); | ||
|
||
return result; | ||
}, | ||
); | ||
} finally { | ||
await flushIfServerless(); | ||
} | ||
} |
Oops, something went wrong.