Wrap server actions in sentry HOF; add custom global-error files#306
Wrap server actions in sentry HOF; add custom global-error files#306
Conversation
| return withServerActionInstrumentation( | ||
| "members/revalidateMemberPathsAndTags", | ||
| { | ||
| headers: headers(), |
There was a problem hiding this comment.
Annoyingly, Sentry's withServerActionInstrumentation accepts a formData object (that must conform to the FormData API) but not an arbitrary payload. This option is completely useless to us since most of our server actions accept primitive/POJO parameters.
I did forward Next's headers along for some additional context, but that's about the best I could do.
There was a problem hiding this comment.
Not that we actually need to add anything in there right now, but is there a reason we couldn't just construct a formData object? like:
const formData = new FormData();
for (const [k, v] of arbitraryPayload.values()) {
formData.append(k, v)
}There was a problem hiding this comment.
That would work, but since we pass multiple parameters and not just single objects with many keys, it could get tricky/annoying to have to name each one.
62f50b5 to
67441b6
Compare
There was a problem hiding this comment.
i'll mark the files here going forward to ask about why not use defineServerAction in these places. if there is a good reason, no problem, but i think if we are going to introduce this pattern (which i think is good!) we implement it all in this PR unless we shouldn't for some reason
There was a problem hiding this comment.
if the reason is "these are integrations and i don't care that much" that's a valid enough reason haha
There was a problem hiding this comment.
Nevermind, the reason is "it's only defined in core", carry on
There was a problem hiding this comment.
nice, this makes me feel somewhat more confident that we won't miss any errors!
There was a problem hiding this comment.
thanks for including specific documentation! very helpful
| "no-restricted-syntax": [ | ||
| "error", | ||
| { | ||
| selector: | ||
| "CallExpression[callee.name='defineServerAction'] > :nth-child(1):not(FunctionExpression[id.name][async=true])", | ||
| message: "You can only pass named, async functions into defineServerAction", | ||
| }, | ||
| ], |
There was a problem hiding this comment.
i added the eslint rule to this PR instead of doing a follow up, seems easier
There was a problem hiding this comment.
(i also resolved the merge conflict that this created)
| * @param serverActionFn | ||
| * @returns | ||
| */ | ||
| export const defineServerAction = <T extends (...args: unknown[]) => Promise<unknown>>( |
There was a problem hiding this comment.
I changed this into Promise<unknown> instead of unknown, to enforce the async-ness of the passed function on the type level as well. (ofc you can pass a non-async-promise-returning function as well, but it's at least a bit closer)
| export function useServerAction<T extends unknown[], U>(action: (...args: T) => Promise<U>) { | ||
| const runServerAction = useCallback( | ||
| async function runServerAction(...args: T) { | ||
| const result = await action(...args); | ||
| if (isClientException(result)) { | ||
| toast({ | ||
| title: result.title ?? "Error", | ||
| variant: "destructive", | ||
| description: `${result.error}${result.id ? ` (Error ID: ${result.id})` : ""}`, | ||
| }); | ||
| } | ||
| return result; | ||
| }, | ||
| [action, toast] | ||
| ); | ||
| return runServerAction; | ||
| } | ||
|
|
|
Seems to work perfectly! I tested this by manually removing a stage in the DB and then trying to remove it in the editor UI, which generated this Sentry error (https://kfg.sentry.io/issues/5162958068/?referrer=alert_email&alert_type=email&alert_timestamp=1712656911734&alert_rule_id=14700197¬ification_uuid=318664ac-f227-408e-af8f-e0c666a07456&environment=development) and showed a toast as intended. Might be a bit hard to test every single server action, but I have faith that this is working as it should! Page errors are also properly sent to sentry
Both were tested by just putting a |
See the server actions explainer for a description of the server actions-related changes.
This PR:
new Sentry.ReplayIntegrationtoSentry.replayIntegration()global-error.tsxfiles to core and integration Next apps. This component should forward all unhandled, non- server action application errors to Sentry.defineServerAction, which wraps server actions withSentry.withServerActionInstrumentationto properly forward uncaught errors in server actions to Sentry. Note, as of now, Sentry's higher-order-function isn't doing much since we're try/catching the result of the server action ourselves.ClientException...ClientException, which are exceptions sent from server actions to the client when something goes wrong in a server action. These objects should help us standardize how we structure error responses and render them on the client.ClientExceptioninterface via a new hook calleduseServerAction. This hook renders theClientExceptionerror message and sentry id (if present).ClientExceptioninstances where possible, and renders the error id in the exception's subsequent toast.Issue(s) Resolved
Resolves #299
Test Plan
if (env.NODE_ENV === "production")check in thesentry.*.config.tsfiles.Screenshots (if applicable)